> describeConstable() {
+ final ClassDesc me = ClassDesc.of(this.getClass().getName());
+ return Constables.describeConstable(this.values())
+ .flatMap(valuesDesc -> Constables.describeConstable(this.notes())
+ .flatMap(notesDesc -> Constables.describeConstable(this.attributes())
+ .map(attributesDesc -> DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ me,
+ "of",
+ MethodTypeDesc.of(me,
+ CD_String,
+ CD_Map,
+ CD_Map,
+ CD_Map)),
+ this.name(),
+ valuesDesc,
+ notesDesc,
+ attributesDesc))));
+ }
+
+ /**
+ * Returns {@code true} if this {@link Attributes} equals the supplied {@link Object}.
+ *
+ * If the supplied {@link Object} is also an {@link Attributes} and has a {@linkplain #name() name} equal to this
+ * {@link Attributes}' {@linkplain #name() name} and a {@linkplain #values() values} {@link Map} {@linkplain
+ * Map#equals(Object) equal to} this {@link Attributes}' {@linkplain #values() values} {@link Map}, this method
+ * returns {@code true}.
+ *
+ * This method returns {@code false} in all other cases.
+ *
+ * @param other an {@link Object}; may be {@code null}
+ *
+ * @return {@code true} if this {@link Attributes} equals the supplied {@link Object}
+ *
+ * @see #hashCode()
+ *
+ * @see #name()
+ *
+ * @see #values()
+ */
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other instanceof Attributes a && this.name().equals(a.name()) && this.values().equals(a.values());
+ }
+
+ /**
+ * Returns a hash code value for this {@link Attributes} derived solely from its {@linkplain #values() values}.
+ *
+ * @return a hash code value
+ *
+ * @see #values()
+ *
+ * @see #equals(Object)
+ */
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ int hashCode = 0;
+ for (final Entry> e : this.values().entrySet()) {
+ hashCode += (127 * e.getKey().hashCode()) ^ e.getValue().hashCode();
+ }
+ return hashCode;
+ }
+
+ /**
+ * Returns {@code true} if {@code a} appears in the {@link #attributes(String) attributes} of this {@link Attributes},
+ * or any of their attributes.
+ *
+ * Notably, this method does not return {@code true} if this {@link Attributes} {@linkplain
+ * #equals(Object) is equal to} {@code a}.
+ *
+ * @param a an {@link Attributes}; must not be {@code null}
+ *
+ * @return {@code true} if {@code a} appears in the {@link #attributes(String) attributes} of this {@link Attributes},
+ * or any of their attributes
+ *
+ * @exception NullPointerException if {@code a} is {@code null}
+ *
+ * @see #attributes(String)
+ *
+ * @see #equals(Object)
+ */
+ public final boolean isa(final Attributes a) {
+ return this.attributesSatisfy(a::equals);
+ }
+
+ /**
+ * Returns {@code true} if any of the {@linkplain #attributes(String) attributes} reachable from this {@link
+ * Attributes} satisfy the supplied {@link Predicate}.
+ *
+ * @param p a {@link Predicate}; must not be {@code null}
+ *
+ * @return {@code true} if any of the {@linkplain #attributes(String) attributes} reachable from this {@link
+ * Attributes} satisfy the supplied {@link Predicate}; {@code false} otherwise
+ *
+ * @exception NullPointerException if {@code p} is {@code null}
+ *
+ * @see #attributes(String)
+ */
+ public final boolean attributesSatisfy(final Predicate super Attributes> p) {
+ for (final Attributes md : this.attributes(this.name())) {
+ if (p.test(md) || md.attributesSatisfy(p)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a suitably-typed {@link Value} indexed under the supplied {@code name}, or {@code null} if no such {@link
+ * Value} exists.
+ *
+ * @param the type of the {@link Value}
+ *
+ * @param name the name; must not be {@code null}
+ *
+ * @return a suitably-typed {@link Value} indexed under the supplied {@code name}, or {@code null} if no such {@link
+ * Value} exists
+ *
+ * @exception NullPointerException if {@code name} is {@code null}
+ *
+ * @exception ClassCastException if {@code } does not match the actual type of the {@link Value} indexed under the
+ * supplied {@code name}
+ */
+ @SuppressWarnings("unchecked")
+ public final > T value(final String name) {
+ return (T)this.values().get(name);
+ }
+
+ /**
+ * Returns an {@link Attributes} comprising the supplied arguments.
+ *
+ * @param name the name; must not be {@code null}
+ *
+ * @return a non-{@code null} {@link Attributes}
+ *
+ * @exception NullPointerException if {@code name} is {@code null}
+ *
+ * @see #of(String, Map, Map, Map)
+ */
+ public static final Attributes of(final String name) {
+ return of(name, Map.of(), Map.of(), Map.of());
+ }
+
+ /**
+ * Returns an {@link Attributes} comprising the supplied arguments.
+ *
+ * @param name the name; must not be {@code null}
+ *
+ * @param valueValue a {@link String} that will be indexed under the key "{@code value}"; must not be {@code null}
+ *
+ * @return a non-{@code null} {@link Attributes}
+ *
+ * @exception NullPointerException if {@code name} or {@code valueValue} is {@code null}
+ *
+ * @see #of(String, Map, Map, Map)
+ */
+ public static final Attributes of(final String name, final String valueValue) {
+ return of(name, Map.of("value", new StringValue(valueValue)), Map.of(), Map.of());
+ }
+
+ /**
+ * Returns an {@link Attributes} comprising the supplied arguments.
+ *
+ * @param name the name; must not be {@code null}
+ *
+ * @param attributes an array of {@link Attributes}; may be {@code null}
+ *
+ * @return a non-{@code null} {@link Attributes}
+ *
+ * @exception NullPointerException if {@code name} is {@code null}
+ *
+ * @see #of(String, List)
+ */
+ public static final Attributes of(final String name, final Attributes... attributes) {
+ return of(name, attributes == null || attributes.length == 0 ? List.of() : Arrays.asList(attributes));
+ }
+
+ /**
+ * Returns an {@link Attributes} comprising the supplied arguments.
+ *
+ * @param name the name; must not be {@code null}
+ *
+ * @param attributes a non-{@code null} {@link List} of {@link Attributes}
+ *
+ * @return a non-{@code null} {@link Attributes}
+ *
+ * @exception NullPointerException if any argument is {@code null}
+ *
+ * @see #of(String, Map, Map, Map)
+ */
+ public static final Attributes of(final String name, final List attributes) {
+ return of(name, Map.of(), Map.of(), Map.of(name, attributes));
+ }
+
+ /**
+ * Returns an {@link Attributes} comprising the supplied arguments.
+ *
+ * @param name the name; must not be {@code null}
+ *
+ * @param values a {@link Map} of {@link Value}s indexed by {@link String} keys; must not be {@code null}
+ *
+ * @param notes a {@link Map} of {@link Value}s indexed by {@link String} keys containing descriptive information
+ * only; must not be {@code null}; not incorporated into equality calculations
+ *
+ * @param attributes a {@link Map} of {@link List}s of {@link Attributes} instances denoting metadata for a given
+ * value in {@code values} (or for this {@link Attributes} as a whole if the key in question is equal to {@code
+ * name}); must not be {@code null}
+ *
+ * @return a non-{@code null} {@link Attributes}
+ *
+ * @exception NullPointerException if any argument is {@code null}
+ */
+ public static final Attributes of(final String name,
+ final Map> values,
+ final Map> notes,
+ final Map> attributes) {
+ return new Attributes(name, values, notes, attributes);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/BooleanValue.java b/src/main/java/org/microbean/attributes/BooleanValue.java
new file mode 100644
index 0000000..628ceef
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/BooleanValue.java
@@ -0,0 +1,104 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.ConstantDesc;
+import java.lang.constant.ConstantDescs;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_boolean;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is a {@code boolean}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record BooleanValue(boolean value) implements Value {
+
+ /**
+ * The {@link BooleanValue} representing {@code true}.
+ */
+ public static final BooleanValue TRUE = new BooleanValue(true);
+
+ /**
+ * The {@link BooleanValue} representing {@code false}.
+ */
+ public static final BooleanValue FALSE = new BooleanValue(false);
+
+ @Override // Comparable
+ public final int compareTo(BooleanValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ } else if (this.value()) {
+ return 1; // false comes first
+ } else {
+ return -1;
+ }
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_boolean)),
+ this.value() ? ConstantDescs.TRUE : ConstantDescs.FALSE));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements
+ other != null && other.getClass() == this.getClass() && this.value() == ((BooleanValue)other).value();
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Boolean.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link BooleanValue} suitable for the supplied arguments.
+ *
+ * @param b a {@code boolean}
+ *
+ * @return {@link TRUE} if {@code b} is {@code true}; {@link FALSE} otherwise
+ */
+ public static final BooleanValue of(final boolean b) {
+ return b ? TRUE : FALSE;
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/ByteValue.java b/src/main/java/org/microbean/attributes/ByteValue.java
new file mode 100644
index 0000000..ffb61e5
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/ByteValue.java
@@ -0,0 +1,96 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_Integer;
+import static java.lang.constant.ConstantDescs.CD_byte;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL;
+
+/**
+ * A {@link Value} whose value is a {@code byte}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record ByteValue(byte value) implements Value {
+
+ @Override // Comparable
+ public final int compareTo(ByteValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_byte)),
+ DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(VIRTUAL,
+ CD_Integer,
+ "byteValue",
+ MethodTypeDesc.of(CD_byte)),
+ Integer.valueOf(this.value()))));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value() == ((ByteValue)other).value();
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Byte.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link ByteValue} suitable for the supplied arguments.
+ *
+ * @param b a {@code byte}
+ *
+ * @return a non-{@code null} {@link ByteValue}
+ */
+ public static final ByteValue of(final byte b) {
+ return new ByteValue(b);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/CharValue.java b/src/main/java/org/microbean/attributes/CharValue.java
new file mode 100644
index 0000000..cc74ec3
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/CharValue.java
@@ -0,0 +1,99 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_String;
+import static java.lang.constant.ConstantDescs.CD_char;
+import static java.lang.constant.ConstantDescs.CD_int;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL;
+
+/**
+ * A {@link Value} whose value is a {@code char}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record CharValue(char value) implements Value {
+
+ @Override // Comparable
+ public final int compareTo(final CharValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_char)),
+ DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(VIRTUAL,
+ CD_String,
+ "charAt",
+ MethodTypeDesc.of(CD_char,
+ CD_int)),
+ String.valueOf(this.value()),
+ Integer.valueOf(0))));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value() == ((CharValue)other).value();
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Character.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link CharValue} suitable for the supplied arguments.
+ *
+ * @param c a {@code char}
+ *
+ * @return a non-{@code null} {@link CharValue}
+ */
+ public static final CharValue of(final char c) {
+ return new CharValue(c);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/ClassValue.java b/src/main/java/org/microbean/attributes/ClassValue.java
new file mode 100644
index 0000000..26d4cb3
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/ClassValue.java
@@ -0,0 +1,110 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_Class;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is a {@code Class}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record ClassValue(Class> value) implements Value {
+
+ public static final ClassValue CLASS_JAVA_LANG_OBJECT = new ClassValue(Object.class);
+
+ public static final ClassValue CLASS_JAVA_LANG_STRING = new ClassValue(String.class);
+
+ /**
+ * Creates a new {@link ClassValue}.
+ *
+ * @param value the value; must not be {@code null}
+ *
+ * @exception NullPointerException if {@code value} is {@code null}
+ */
+ public ClassValue {
+ Objects.requireNonNull(value, "value");
+ }
+
+ @Override // Comparable
+ public final int compareTo(final ClassValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value().getName().compareTo(other.value().getName());
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_Class)),
+ this.value().describeConstable().orElseThrow()));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value().equals(((ClassValue)other).value());
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return this.value().hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return this.value().getName(); // binary name
+ }
+
+ /**
+ * Returns a {@link ClassValue} suitable for the supplied arguments.
+ *
+ * @param c a {@code Class}; must not be {@code null}
+ *
+ * @return a non-{@code null} {@link ClassValue}
+ *
+ * @exception NullPointerException if {@code c} is {@code null}
+ */
+ public static final ClassValue of(final Class> c) {
+ return
+ c == Object.class ? CLASS_JAVA_LANG_OBJECT :
+ c == String.class ? CLASS_JAVA_LANG_STRING :
+ new ClassValue(c);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/DoubleValue.java b/src/main/java/org/microbean/attributes/DoubleValue.java
new file mode 100644
index 0000000..eaa0a62
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/DoubleValue.java
@@ -0,0 +1,90 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.ConstantDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_double;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is a {@code double}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record DoubleValue(double value) implements Value {
+
+ @Override // Comparable
+ public final int compareTo(final DoubleValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_double)),
+ Double.valueOf(this.value())));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && Double.valueOf(this.value()).equals(Double.valueOf(((DoubleValue)other).value()));
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Double.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link DoubleValue} suitable for the supplied arguments.
+ *
+ * @param d a {@code double}
+ *
+ * @return a non-{@code null} {@link DoubleValue}
+ */
+ public static final DoubleValue of(final double d) {
+ return new DoubleValue(d);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/EnumValue.java b/src/main/java/org/microbean/attributes/EnumValue.java
new file mode 100644
index 0000000..b9e6c54
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/EnumValue.java
@@ -0,0 +1,105 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_Enum;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is an {@code Enum}.
+ *
+ * @param the type of the {@link Enum}
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record EnumValue>(E value) implements Value> {
+
+ /**
+ * Creates a new {@link EnumValue}.
+ *
+ * @param value the value; must not be {@code null}
+ *
+ * @exception NullPointerException if {@code value} is {@code null}
+ */
+ public EnumValue {
+ Objects.requireNonNull(value, "value");
+ }
+
+ @Override // Comparable
+ public final int compareTo(final EnumValue other) {
+ return
+ other == null ? -1 :
+ this.equals(other) ? 0 :
+ this.value().compareTo(other.value());
+ }
+
+ @Override // Constable
+ public final Optional>> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_Enum)),
+ this.value().describeConstable().orElseThrow()));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value().equals(((EnumValue>)other).value());
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return this.value().hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return this.value().toString();
+ }
+
+ /**
+ * Returns an {@link EnumValue} suitable for the supplied arguments.
+ *
+ * @param the type of the {@link Enum}
+ *
+ * @param e a non-{@code null} {@code Enum}
+ *
+ * @return a non-{@code null} {@link EnumValue}
+ *
+ * @exception NullPointerException if {@code e} is {@code null}
+ */
+ public static final > EnumValue of(final E e) {
+ return new EnumValue<>(e);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/FloatValue.java b/src/main/java/org/microbean/attributes/FloatValue.java
new file mode 100644
index 0000000..777120e
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/FloatValue.java
@@ -0,0 +1,89 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_float;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is a {@code float}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record FloatValue(float value) implements Value {
+
+ @Override // Comparable
+ public final int compareTo(final FloatValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_float)),
+ Float.valueOf(this.value())));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && Float.valueOf(this.value()).equals(Float.valueOf(((FloatValue)other).value()));
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Float.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link FloatValue} suitable for the supplied arguments.
+ *
+ * @param f a {@code float}
+ *
+ * @return a non-{@code null} {@link FloatValue}
+ */
+ public static final FloatValue of(final float f) {
+ return new FloatValue(f);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/IntValue.java b/src/main/java/org/microbean/attributes/IntValue.java
new file mode 100644
index 0000000..e84bf54
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/IntValue.java
@@ -0,0 +1,100 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_int;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is an {@code int}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record IntValue(int value) implements Value {
+
+ public static final IntValue NEGATIVE_ONE = new IntValue(-1);
+
+ public static final IntValue ZERO = new IntValue(0);
+
+ public static final IntValue ONE = new IntValue(1);
+
+ @Override // Comparable
+ public final int compareTo(final IntValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_int)),
+ Integer.valueOf(this.value())));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value() == ((IntValue)other).value();
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Integer.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns an {@link IntValue} suitable for the supplied arguments.
+ *
+ * @param i am {@code int}
+ *
+ * @return a non-{@code null} {@link IntValue}
+ */
+ public static final IntValue of(final int i) {
+ return switch (i) {
+ case -1 -> NEGATIVE_ONE;
+ case 0 -> ZERO;
+ case 1 -> ONE;
+ default -> new IntValue(i);
+ };
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/LongValue.java b/src/main/java/org/microbean/attributes/LongValue.java
new file mode 100644
index 0000000..ac17f3f
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/LongValue.java
@@ -0,0 +1,89 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_long;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is a {@code long}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record LongValue(long value) implements Value {
+
+ @Override // Comparable
+ public final int compareTo(final LongValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_long)),
+ Long.valueOf(this.value())));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value() == ((LongValue)other).value();
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Long.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link LongValue} suitable for the supplied arguments.
+ *
+ * @param l a {@code long}
+ *
+ * @return a non-{@code null} {@link LongValue}
+ */
+ public static final LongValue of(final long l) {
+ return new LongValue(l);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/ShortValue.java b/src/main/java/org/microbean/attributes/ShortValue.java
new file mode 100644
index 0000000..5dbd8c8
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/ShortValue.java
@@ -0,0 +1,96 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_Integer;
+import static java.lang.constant.ConstantDescs.CD_short;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.VIRTUAL;
+
+/**
+ * A {@link Value} whose value is a {@code short}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record ShortValue(short value) implements Value {
+
+ @Override // Comparable
+ public final int compareTo(final ShortValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value() > other.value() ? 1 : -1;
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_short)),
+ DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(VIRTUAL,
+ CD_Integer,
+ "shortValue",
+ MethodTypeDesc.of(CD_short)),
+ Integer.valueOf(this.value()))));
+ }
+
+ @Override // Record
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value() == ((ShortValue)other).value();
+ }
+
+ @Override // Record
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return Short.valueOf(this.value()).hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return String.valueOf(this.value());
+ }
+
+ /**
+ * Returns a {@link ShortValue} suitable for the supplied arguments.
+ *
+ * @param s a {@code short}
+ *
+ * @return a non-{@code null} {@link ShortValue}
+ */
+ public static final ShortValue of(final short s) {
+ return new ShortValue(s);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/StringValue.java b/src/main/java/org/microbean/attributes/StringValue.java
new file mode 100644
index 0000000..adcc6cb
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/StringValue.java
@@ -0,0 +1,104 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.ClassDesc;
+import java.lang.constant.DynamicConstantDesc;
+import java.lang.constant.MethodHandleDesc;
+import java.lang.constant.MethodTypeDesc;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import static java.lang.constant.ConstantDescs.BSM_INVOKE;
+import static java.lang.constant.ConstantDescs.CD_String;
+import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC;
+
+/**
+ * A {@link Value} whose value is a {@code String}.
+ *
+ * @param value the value
+ *
+ * @author Laird Nelson
+ */
+public final record StringValue(String value) implements Value {
+
+ public static final StringValue EMPTY = new StringValue("");
+
+ /**
+ * Creates a new {@link StringValue}.
+ *
+ * @param value the value; must not be {@code null}
+ *
+ * @exception NullPointerException if {@code value} is {@code null}
+ */
+ public StringValue {
+ Objects.requireNonNull(value, "value");
+ }
+
+ @Override // Comparable
+ public final int compareTo(final StringValue other) {
+ if (other == null) {
+ return -1;
+ } else if (this.equals(other)) {
+ return 0;
+ }
+ return this.value().compareTo(other.value());
+ }
+
+ @Override // Constable
+ public final Optional> describeConstable() {
+ final ClassDesc cd = ClassDesc.of(this.getClass().getName());
+ return
+ Optional.of(DynamicConstantDesc.of(BSM_INVOKE,
+ MethodHandleDesc.ofMethod(STATIC,
+ cd,
+ "of",
+ MethodTypeDesc.of(cd,
+ CD_String)),
+ this.value()));
+ }
+
+ @Override
+ public final boolean equals(final Object other) {
+ return
+ other == this ||
+ // Follow java.lang.annotation.Annotation requirements.
+ other != null && other.getClass() == this.getClass() && this.value().equals(((StringValue)other).value());
+ }
+
+ public final int hashCode() {
+ // Follow java.lang.annotation.Annotation requirements.
+ return this.value().hashCode();
+ }
+
+ @Override // Record
+ public final String toString() {
+ return this.value();
+ }
+
+ /**
+ * Returns a {@link StringValue} suitable for the supplied arguments.
+ *
+ * @param s a non-{@code null} {@code String}
+ *
+ * @return a non-{@code null} {@link StringValue}
+ *
+ * @exception NullPointerException if {@code s} is {@code null}
+ */
+ public static final StringValue of(final String s) {
+ return s.isEmpty() ? EMPTY : new StringValue(s);
+ }
+
+}
diff --git a/src/main/java/org/microbean/attributes/Value.java b/src/main/java/org/microbean/attributes/Value.java
new file mode 100644
index 0000000..f7645c0
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/Value.java
@@ -0,0 +1,70 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.constant.Constable;
+
+/**
+ * A value of a particular type suitable for representing as an annotation value or similar.
+ *
+ * @param a {@link Value} subtype
+ *
+ * {@link Value}s are value-based
+ * classes.
+ *
+ * @author Laird Nelson
+ *
+ * @see ArrayValue
+ *
+ * @see Attributes
+ *
+ * @see BooleanValue
+ *
+ * @see ByteValue
+ *
+ * @see CharValue
+ *
+ * @see ClassValue
+ *
+ * @see DoubleValue
+ *
+ * @see EnumValue
+ *
+ * @see FloatValue
+ *
+ * @see IntValue
+ *
+ * @see LongValue
+ *
+ * @see ShortValue
+ *
+ * @see StringValue
+ */
+public sealed interface Value> extends Comparable, Constable
+ permits ArrayValue,
+ Attributes,
+ BooleanValue,
+ ByteValue,
+ CharValue,
+ ClassValue,
+ DoubleValue,
+ EnumValue,
+ FloatValue,
+ IntValue,
+ LongValue,
+ ShortValue,
+ StringValue {
+
+}
diff --git a/src/main/java/org/microbean/attributes/package-info.java b/src/main/java/org/microbean/attributes/package-info.java
new file mode 100644
index 0000000..281cb9b
--- /dev/null
+++ b/src/main/java/org/microbean/attributes/package-info.java
@@ -0,0 +1,20 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+/**
+ * Provides classes and interfaces related to representing annotations.
+ *
+ * @author Laird Nelson
+ */
+package org.microbean.attributes;
diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm
new file mode 100644
index 0000000..debee59
--- /dev/null
+++ b/src/site/markdown/index.md.vm
@@ -0,0 +1,2 @@
+#include("../../../README.md")
+
diff --git a/src/site/site.xml b/src/site/site.xml
new file mode 100644
index 0000000..fb69fe9
--- /dev/null
+++ b/src/site/site.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ org.apache.maven.skins
+ maven-fluido-skin
+ 2.0.0
+
+
+
+
+
+
+
+ true
+ false
+
+
+
diff --git a/src/test/java/org/microbean/attributes/TestAttributes.java b/src/test/java/org/microbean/attributes/TestAttributes.java
new file mode 100644
index 0000000..df3ceb0
--- /dev/null
+++ b/src/test/java/org/microbean/attributes/TestAttributes.java
@@ -0,0 +1,41 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestAttributes {
+
+ private TestAttributes() {
+ super();
+ }
+
+ @Test
+ final void testAttributes() {
+ final Attributes documented = Attributes.of("Documented");
+ final Attributes qualifier = Attributes.of("Qualifier", documented);
+ final Attributes named = Attributes.of("Named", qualifier);
+ assertFalse(named.isa(named));
+ assertTrue(named.isa(qualifier));
+ assertTrue(qualifier.isa(documented));
+ assertTrue(named.isa(documented));
+ }
+
+}
diff --git a/src/test/java/org/microbean/attributes/TestConstableSemantics.java b/src/test/java/org/microbean/attributes/TestConstableSemantics.java
new file mode 100644
index 0000000..5529a63
--- /dev/null
+++ b/src/test/java/org/microbean/attributes/TestConstableSemantics.java
@@ -0,0 +1,141 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2025 microBean™.
+ *
+ * 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
+ *
+ * http://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 org.microbean.attributes;
+
+import java.lang.annotation.RetentionPolicy;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+import static java.lang.invoke.MethodHandles.lookup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+final class TestConstableSemantics {
+
+ private TestConstableSemantics() {
+ super();
+ }
+
+ @Test
+ final void testArrayValue() throws ReflectiveOperationException {
+ final ArrayValue v = ArrayValue.of("a", "b", "c");
+ final ArrayValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testAttributes() throws ReflectiveOperationException {
+ final Attributes v = Attributes.of("Qualifier");
+ final Attributes v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testBooleanValue() throws ReflectiveOperationException {
+ final BooleanValue v = BooleanValue.of(false);
+ final BooleanValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertSame(v, v0); // note
+ }
+
+ @Test
+ final void testByteValue() throws ReflectiveOperationException {
+ final ByteValue v = ByteValue.of((byte)1);
+ final ByteValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testCharValue() throws ReflectiveOperationException {
+ final CharValue v = CharValue.of('a');
+ final CharValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testClassValue() throws ReflectiveOperationException {
+ final ClassValue v = ClassValue.of(Long.class);
+ final ClassValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+
+ @Test
+ final void testDoubleValue() throws ReflectiveOperationException {
+ final DoubleValue v = DoubleValue.of(2D);
+ final DoubleValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testEnumValue() throws ReflectiveOperationException {
+ final EnumValue v = EnumValue.of(RetentionPolicy.RUNTIME);
+ final EnumValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testFloatValue() throws ReflectiveOperationException {
+ final FloatValue v = FloatValue.of(2F);
+ final FloatValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testIntValue() throws ReflectiveOperationException {
+ final IntValue v = IntValue.of(2);
+ final IntValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+
+ @Test
+ final void testLongValue() throws ReflectiveOperationException {
+ final LongValue v = LongValue.of(2L);
+ final LongValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testShortValue() throws ReflectiveOperationException {
+ final ShortValue v = ShortValue.of((short)2);
+ final ShortValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+ @Test
+ final void testStringValue() throws ReflectiveOperationException {
+ final StringValue v = StringValue.of("a");
+ final StringValue v0 = v.describeConstable().orElseThrow().resolveConstantDesc(lookup());
+ assertEquals(v, v0);
+ assertNotSame(v, v0);
+ }
+
+}