Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (core): ByteSeq class #478

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions core/lib/src/main/java/dev/enola/core/ByteSeq.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2023-2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.core;

import com.google.protobuf.ByteString;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.UUID;

/**
* Sequence of Bytes, of variable length, and immutable; often used as an Enola Type / Entity ID. Do
* not use this for "BLOBs" (like images or so). The hashCode is cached.
*
* <p>In Enola, these IDs are intended to be unique, for a given (possibly clustered) instance.
* Federated Enola instances in theory could have duplicates, but with sufficiently long randomly
* generated bytes this is considered rare enough in practice that you can assume these are globally
* unique.
*/
// TODO Make this a (the first!) Enola "simple type", with verbs for create & asUUID!
// TODO Link to docs/**/enola.md once that's published on https://docs.enola.dev.
public final class ByteSeq implements Comparable<ByteSeq> {

public static final ByteSeq EMPTY = new ByteSeq(new byte[0]);

public static final class Builder {
private final byte[] bytes;

private Builder(int size) {
bytes = new byte[size];
}

public ByteSeq build() {
return new ByteSeq(bytes);
}

public Builder add(ByteBuffer bb) {
if (!bb.isReadOnly()) {
throw new IllegalArgumentException("ByteBuffer !isReadOnly()");
}
bb.get(bytes);
return this;
}
}

public static Builder builder(int size) {
return new Builder(size);
}

// TODO public static final ByteSeq fromMultibase(String multibase) {

/**
* Create a ByteSeq from an array of bytes. Prefer using the Builder instead of this, to avoid
* the implementation have to copy the array.
*
* @param bytes Bytes (which will be copied)
* @return the ByteSeq
*/
public static final ByteSeq from(byte[] bytes) {
if (bytes.length == 0) {
return EMPTY;
}
return new ByteSeq(Arrays.copyOf(bytes, bytes.length));
}

/**
* Create a ByteSeq from a Protocol Buffer "bytes" field.
*
* @param proto the ByteString
* @return the ByteSeq
*/
public static ByteSeq from(ByteString proto) {
return ByteSeq.builder(proto.size()).add(proto.asReadOnlyByteBuffer()).build();
}

// ? public static ByteSeq toByteSeq(Message proto) { return
// ByteSeq.copyFrom(proto.toByteArray()); }

public static ByteSeq from(UUID uuid) {
var byteBuffer = ByteBuffer.allocate(16);
byteBuffer.putLong(uuid.getMostSignificantBits());
byteBuffer.putLong(uuid.getLeastSignificantBits());
byteBuffer.position(0);

var builder = builder(16);
builder.add(byteBuffer.asReadOnlyBuffer());
return builder.build();
}

private final byte[] bytes;
private transient int hashCode;

private ByteSeq(byte[] bytes) {
this.bytes = bytes;
}

public byte[] toBytes() {
return Arrays.copyOf(bytes, bytes.length);
}

public int size() {
return bytes.length;
}

public byte get(int index) {
return bytes[index];
}

// public String toString(Base base) { return Multibase.encode(Base.Base32, id.toByteArray()); }

public ByteString toByteString() {
return ByteString.copyFrom(bytes);
}

public UUID toUUID() {
if (bytes.length != 16) {
throw new IllegalStateException(
"toUUID() is currently only supported for length == 16, not: " + bytes.length);
}
ByteBuffer bb = ByteBuffer.wrap(bytes);
long high = bb.getLong();
long low = bb.getLong();
return new UUID(high, low);
}

@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = Arrays.hashCode(bytes);
}
return hashCode;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
var other = (ByteSeq) obj;
if (hashCode() != other.hashCode()) {
return false;
}
return Arrays.equals(bytes, other.bytes);
}

@Override
public int compareTo(ByteSeq other) {
return Arrays.compare(bytes, other.bytes);
}
}
80 changes: 80 additions & 0 deletions core/lib/src/test/java/dev/enola/core/ByteSeqTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2023-2024 The Enola <https://enola.dev> Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.enola.core;

import static com.google.common.truth.Truth.assertThat;

import com.google.protobuf.ByteString;

import org.junit.Test;

import java.util.UUID;

public class ByteSeqTest {

@Test
public void byteArray() {
var array = new byte[] {1, 2, 3};
var id = ByteSeq.from(array);

assertThat(id.size()).isEqualTo(3);
assertThat(id.toBytes()).isEqualTo(array);
assertThat(id.get(1)).isEqualTo(2);
assertThat(id.hashCode()).isEqualTo(30817);
assertThat(id.equals(ByteSeq.from(array))).isTrue();

array[1] = 7;
assertThat(id.get(1)).isEqualTo(2);
}

@Test
public void uuid() {
var uuid = UUID.randomUUID();
var id = ByteSeq.from(uuid);

assertThat(id.size()).isEqualTo(16);
assertThat(id.toUUID()).isEqualTo(uuid);
assertThat(id.toUUID().toString().length()).isEqualTo(36);
}

@Test
public void protobufByteString() {
var byteString = ByteString.copyFromUtf8("hello, world 😃");
var id = ByteSeq.from(byteString);

assertThat(id.size()).isEqualTo(17);
assertThat(id.hashCode()).isEqualTo(734434573);
assertThat(id.toByteString()).isEqualTo(byteString);
}

@Test
public void compare() {
var id1 = ByteSeq.from(new byte[] {1});
var id2 = ByteSeq.from(new byte[] {1, 2});

assertThat(id1.compareTo(id2)).isEqualTo(-1);
}

@Test
public void empty() {
assertThat(ByteSeq.EMPTY.size()).isEqualTo(0);
assertThat(ByteSeq.EMPTY.toBytes()).isEqualTo(new byte[0]);
assertThat(ByteSeq.EMPTY.hashCode()).isEqualTo(1);
// TODOassertThat(ByteSeq.EMPTY.toUUID().toString()).isEqualTo("...");
}
}