This is an automated email from the ASF dual-hosted git repository.

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 02cafdb0da [#12163] feat(api): Add tag assignment value APIs (#12162)
02cafdb0da is described below

commit 02cafdb0da00b3949193d68476d7414aa8648c85
Author: roryqi <[email protected]>
AuthorDate: Tue Jul 28 16:40:03 2026 +0800

    [#12163] feat(api): Add tag assignment value APIs (#12162)
    
    ### What changes were proposed in this pull request?
    
    Add the public tag assignment value APIs with `TagValue`, tag allowed
    values, and valued tag association overloads.
    
    ### Why are the changes needed?
    
    This is the API layer for supporting multiple assignment values for a
    tag.
    
    Fix: #12163
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. It adds `TagValue`, exposes tag allowed values, and adds valued tag
    association APIs while keeping existing valueless tag association APIs.
    
    ### How was this patch tested?
    
    `./gradlew :api:spotlessApply`
    `./gradlew :api:test --tests org.apache.gravitino.tag.TestTagValue
    -PskipITs -PskipDockerTests=false`
---
 .../org/apache/gravitino/tag/SupportsTags.java     |  29 ++--
 .../main/java/org/apache/gravitino/tag/Tag.java    |  30 ++++
 .../org/apache/gravitino/tag/TagAssignment.java    | 113 +++++++++++++++
 .../org/apache/gravitino/tag/TagOperations.java    |  25 ++++
 .../java/org/apache/gravitino/tag/TagValue.java    | 114 +++++++++++++++
 .../apache/gravitino/tag/TagValueConstraint.java   | 158 +++++++++++++++++++++
 .../apache/gravitino/tag/TestTagAssignment.java    |  69 +++++++++
 .../org/apache/gravitino/tag/TestTagValue.java     |  86 +++++++++++
 .../gravitino/tag/TestTagValueConstraint.java      |  89 ++++++++++++
 9 files changed, 705 insertions(+), 8 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/tag/SupportsTags.java 
b/api/src/main/java/org/apache/gravitino/tag/SupportsTags.java
index 0a909c4c8c..5d75cd139d 100644
--- a/api/src/main/java/org/apache/gravitino/tag/SupportsTags.java
+++ b/api/src/main/java/org/apache/gravitino/tag/SupportsTags.java
@@ -54,17 +54,30 @@ public interface SupportsTags {
   Tag getTag(String name) throws NoSuchTagException;
 
   /**
-   * Associate tags to the specific object. The tagsToAdd will be added to the 
object, and the
-   * tagsToRemove will be removed from the object. Note that: 1) Adding or 
removing tags that are
-   * not existed will be ignored. 2) If the same name tag is in both tagsToAdd 
and tagsToRemove, it
-   * will be ignored. 3) If the tag is already associated with the object, it 
will throw {@link
-   * TagAlreadyAssociatedException}
+   * Associate tags with no value to the specific object. The tagsToAdd will 
be added to the object,
+   * and the tagsToRemove will be removed from the object. Missing tags are 
ignored. If the same tag
+   * is in both tagsToAdd and tagsToRemove, it will be ignored. Repeated 
existing assignments are
+   * idempotent.
    *
-   * @param tagsToAdd The arrays of tag name to be added to the object.
-   * @param tagsToRemove The array of tag name to be removed from the object.
+   * @param tagsToAdd The array of tag names to be added to the object.
+   * @param tagsToRemove The array of tag names to be removed from the object.
    * @return The array of tag names that are associated with the object.
-   * @throws TagAlreadyAssociatedException If the tag is already associated 
with the object.
+   * @throws TagAlreadyAssociatedException If the association cannot be 
applied.
    */
   String[] associateTags(String[] tagsToAdd, String[] tagsToRemove)
       throws TagAlreadyAssociatedException;
+
+  /**
+   * Associate tag values to the specific object.
+   *
+   * @param tagsToAdd The tag values to be added to the object.
+   * @param tagsToRemove The tag values to be removed from the object.
+   * @return The array of tag names that are associated with the object.
+   * @throws TagAlreadyAssociatedException If the association cannot be 
applied.
+   */
+  default String[] associateTags(TagValue[] tagsToAdd, TagValue[] tagsToRemove)
+      throws TagAlreadyAssociatedException {
+    throw new UnsupportedOperationException(
+        "The associateTags(TagValue[], TagValue[]) method is not supported.");
+  }
 }
diff --git a/api/src/main/java/org/apache/gravitino/tag/Tag.java 
b/api/src/main/java/org/apache/gravitino/tag/Tag.java
index c2c107dca6..f17d877e07 100644
--- a/api/src/main/java/org/apache/gravitino/tag/Tag.java
+++ b/api/src/main/java/org/apache/gravitino/tag/Tag.java
@@ -54,6 +54,26 @@ public interface Tag extends Auditable {
    */
   Map<String, String> properties();
 
+  /**
+   * @return The assignment value constraint of the tag. The default means the 
tag has no value
+   *     restriction and may be assigned without values or with any non-empty 
assignment value.
+   */
+  default TagValueConstraint valueConstraint() {
+    return TagValueConstraint.anyValue();
+  }
+
+  /**
+   * Returns the tag assignment in the current metadata-object context.
+   *
+   * <p>The return value is present only when the tag is returned for a 
metadata object. A present
+   * assignment without values means the tag is directly or effectively 
assigned without values.
+   *
+   * @return The assignment in context, or empty when no assignment context 
exists.
+   */
+  default Optional<TagAssignment> assignment() {
+    return Optional.empty();
+  }
+
   /**
    * Check if the tag is inherited from a parent object or not. If the tag is 
inherited, it will
    * return true, if it is owned by the object itself, it will return false.
@@ -88,5 +108,15 @@ public interface Tag extends Auditable {
      * @return The list of objects that are associated with this tag.
      */
     MetadataObject[] objects();
+
+    /**
+     * List objects associated with this tag and assignment value.
+     *
+     * @param value The exact assignment value to filter by.
+     * @return The list of objects that are associated with this tag and value.
+     */
+    default MetadataObject[] objects(String value) {
+      throw new UnsupportedOperationException("The objects(value) method is 
not supported.");
+    }
   }
 }
diff --git a/api/src/main/java/org/apache/gravitino/tag/TagAssignment.java 
b/api/src/main/java/org/apache/gravitino/tag/TagAssignment.java
new file mode 100644
index 0000000000..7f350807a5
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/tag/TagAssignment.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.gravitino.tag;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/** Represents a tag assignment in a metadata-object context. */
+@Evolving
+public final class TagAssignment {
+
+  private static final TagAssignment NO_VALUE = new TagAssignment(new 
String[0]);
+
+  private final String[] values;
+
+  private TagAssignment(String[] values) {
+    this.values = values.clone();
+  }
+
+  /**
+   * Creates an assignment with no value.
+   *
+   * @return The tag assignment.
+   */
+  public static TagAssignment noValue() {
+    return NO_VALUE;
+  }
+
+  /**
+   * Creates an assignment with values.
+   *
+   * @param values The assignment values.
+   * @return The tag assignment.
+   */
+  public static TagAssignment ofValues(String... values) {
+    Preconditions.checkArgument(
+        values != null && values.length > 0, "Assignment values must not be 
null or empty");
+    for (String value : values) {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(value), "Assignment values must not contain 
blank values");
+    }
+
+    return new TagAssignment(values);
+  }
+
+  /**
+   * @return True if the assignment has one or more values.
+   */
+  public boolean hasValues() {
+    return values.length > 0;
+  }
+
+  /**
+   * @return The assignment values. Empty when this assignment has no value.
+   */
+  public String[] values() {
+    return values.clone();
+  }
+
+  /**
+   * Compares this assignment with another object.
+   *
+   * @param o The object to compare.
+   * @return True if the object is equal to this assignment.
+   */
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TagAssignment)) {
+      return false;
+    }
+
+    TagAssignment that = (TagAssignment) o;
+    return Arrays.equals(values, that.values);
+  }
+
+  /**
+   * @return The hash code of this assignment.
+   */
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(values);
+  }
+
+  /**
+   * @return The string representation of this assignment.
+   */
+  @Override
+  public String toString() {
+    return "TagAssignment{" + "values=" + Arrays.toString(values) + "}";
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/tag/TagOperations.java 
b/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
index c58ceeda8e..ea9045d92b 100644
--- a/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
+++ b/api/src/main/java/org/apache/gravitino/tag/TagOperations.java
@@ -70,6 +70,31 @@ public interface TagOperations {
   Tag createTag(String name, String comment, Map<String, String> properties)
       throws TagAlreadyExistsException;
 
+  /**
+   * Create a tag under a metalake with an assignment value constraint.
+   *
+   * @param name The name of the tag.
+   * @param comment The comment of the tag.
+   * @param properties The properties of the tag.
+   * @param valueConstraint The assignment value constraint of the tag.
+   * @return The created tag.
+   * @throws TagAlreadyExistsException If the tag already exists.
+   * @throws UnsupportedOperationException If non-default value constraints 
are not supported.
+   */
+  default Tag createTag(
+      String name,
+      String comment,
+      Map<String, String> properties,
+      TagValueConstraint valueConstraint)
+      throws TagAlreadyExistsException {
+    if (valueConstraint == null || 
TagValueConstraint.anyValue().equals(valueConstraint)) {
+      return createTag(name, comment, properties);
+    }
+
+    throw new UnsupportedOperationException(
+        "Creating tags with value constraints is not supported");
+  }
+
   /**
    * Alter a tag under a metalake.
    *
diff --git a/api/src/main/java/org/apache/gravitino/tag/TagValue.java 
b/api/src/main/java/org/apache/gravitino/tag/TagValue.java
new file mode 100644
index 0000000000..919aa81dd8
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/tag/TagValue.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.gravitino.tag;
+
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/** Represents one tag assignment value in tag association requests. */
+@Evolving
+public final class TagValue {
+
+  private final String name;
+
+  @Nullable private final String value;
+
+  private TagValue(String name, @Nullable String value) {
+    this.name = name;
+    this.value = value;
+  }
+
+  /**
+   * Creates a valued tag assignment value.
+   *
+   * @param name The tag name.
+   * @param value The assignment value.
+   * @return The tag value.
+   */
+  public static TagValue of(String name, String value) {
+    Preconditions.checkArgument(StringUtils.isNotBlank(name), "Tag name must 
not be blank");
+    Preconditions.checkArgument(StringUtils.isNotBlank(value), "Tag value must 
not be blank");
+    return new TagValue(name, value);
+  }
+
+  /**
+   * Creates a tag assignment with no value.
+   *
+   * @param name The tag name.
+   * @return The tag value.
+   */
+  public static TagValue noValue(String name) {
+    Preconditions.checkArgument(StringUtils.isNotBlank(name), "Tag name must 
not be blank");
+    return new TagValue(name, null);
+  }
+
+  /**
+   * @return The tag name.
+   */
+  public String name() {
+    return name;
+  }
+
+  /**
+   * @return The optional assignment value.
+   */
+  public Optional<String> value() {
+    return Optional.ofNullable(value);
+  }
+
+  /**
+   * Compares this value with another object.
+   *
+   * @param o The object to compare.
+   * @return True if the object is equal to this value.
+   */
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TagValue)) {
+      return false;
+    }
+
+    TagValue that = (TagValue) o;
+    return Objects.equals(name, that.name) && Objects.equals(value, 
that.value);
+  }
+
+  /**
+   * @return The hash code of this tag value.
+   */
+  @Override
+  public int hashCode() {
+    return Objects.hash(name, value);
+  }
+
+  /**
+   * @return The string representation of this tag value.
+   */
+  @Override
+  public String toString() {
+    return "TagValue{" + "name=" + name + ", value=" + value + "}";
+  }
+}
diff --git a/api/src/main/java/org/apache/gravitino/tag/TagValueConstraint.java 
b/api/src/main/java/org/apache/gravitino/tag/TagValueConstraint.java
new file mode 100644
index 0000000000..49f68040be
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/tag/TagValueConstraint.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.gravitino.tag;
+
+import com.google.common.base.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.annotation.Evolving;
+
+/** Describes what assignment values a tag accepts. */
+@Evolving
+public final class TagValueConstraint {
+
+  private static final TagValueConstraint ANY_VALUE =
+      new TagValueConstraint(Type.ANY_VALUE, new String[0]);
+
+  private static final TagValueConstraint NO_VALUE =
+      new TagValueConstraint(Type.NO_VALUE, new String[0]);
+
+  private final Type type;
+  private final String[] allowedValues;
+
+  private TagValueConstraint(Type type, String[] allowedValues) {
+    this.type = type;
+    this.allowedValues = allowedValues.clone();
+  }
+
+  /** The value constraint type. */
+  public enum Type {
+    /**
+     * The tag has no assignment value restriction and accepts assignments 
with no value or with
+     * values.
+     */
+    ANY_VALUE,
+
+    /** The tag only accepts assignments with no value. */
+    NO_VALUE,
+
+    /** The tag only accepts values from the allowed value list. */
+    ALLOWED_VALUES
+  }
+
+  /**
+   * Creates a constraint with no assignment value restriction.
+   *
+   * <p>The tag may be assigned with no value or with any non-empty assignment 
value.
+   *
+   * @return The value constraint.
+   */
+  public static TagValueConstraint anyValue() {
+    return ANY_VALUE;
+  }
+
+  /**
+   * Creates a constraint that only accepts assignments with no value.
+   *
+   * @return The value constraint.
+   */
+  public static TagValueConstraint noValue() {
+    return NO_VALUE;
+  }
+
+  /**
+   * Creates a constraint that only accepts values from the allowed value list.
+   *
+   * @param allowedValues The allowed assignment values.
+   * @return The value constraint.
+   */
+  public static TagValueConstraint ofAllowedValues(String... allowedValues) {
+    Preconditions.checkArgument(
+        allowedValues != null && allowedValues.length > 0,
+        "Allowed values must not be null or empty");
+    for (String allowedValue : allowedValues) {
+      Preconditions.checkArgument(
+          StringUtils.isNotBlank(allowedValue), "Allowed values must not 
contain blank values");
+    }
+
+    return new TagValueConstraint(Type.ALLOWED_VALUES, allowedValues);
+  }
+
+  /**
+   * @return The value constraint type.
+   */
+  public Type type() {
+    return type;
+  }
+
+  /**
+   * @return The allowed values. Empty when the type is not {@link 
Type#ALLOWED_VALUES}.
+   */
+  public String[] allowedValues() {
+    return allowedValues.clone();
+  }
+
+  /**
+   * Compares this constraint with another object.
+   *
+   * @param o The object to compare.
+   * @return True if the object is equal to this constraint.
+   */
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (!(o instanceof TagValueConstraint)) {
+      return false;
+    }
+
+    TagValueConstraint that = (TagValueConstraint) o;
+    return type == that.type && Arrays.equals(allowedValues, 
that.allowedValues);
+  }
+
+  /**
+   * @return The hash code of this constraint.
+   */
+  @Override
+  public int hashCode() {
+    int result = Objects.hash(type);
+    result = 31 * result + Arrays.hashCode(allowedValues);
+    return result;
+  }
+
+  /**
+   * @return The string representation of this constraint.
+   */
+  @Override
+  public String toString() {
+    if (type != Type.ALLOWED_VALUES) {
+      return "TagValueConstraint{" + "type=" + type + "}";
+    }
+
+    return "TagValueConstraint{"
+        + "type="
+        + type
+        + ", allowedValues="
+        + Arrays.toString(allowedValues)
+        + "}";
+  }
+}
diff --git a/api/src/test/java/org/apache/gravitino/tag/TestTagAssignment.java 
b/api/src/test/java/org/apache/gravitino/tag/TestTagAssignment.java
new file mode 100644
index 0000000000..e977aa0412
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/tag/TestTagAssignment.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.gravitino.tag;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagAssignment {
+
+  @Test
+  void testNoValueAssignment() {
+    TagAssignment assignment = TagAssignment.noValue();
+
+    Assertions.assertFalse(assignment.hasValues());
+    Assertions.assertArrayEquals(new String[0], assignment.values());
+  }
+
+  @Test
+  void testAssignmentWithValues() {
+    String[] values = new String[] {"finance", "risk"};
+    TagAssignment assignment = TagAssignment.ofValues(values);
+    values[0] = "changed";
+
+    Assertions.assertTrue(assignment.hasValues());
+    Assertions.assertArrayEquals(new String[] {"finance", "risk"}, 
assignment.values());
+
+    String[] returnedValues = assignment.values();
+    returnedValues[0] = "changed";
+    Assertions.assertArrayEquals(new String[] {"finance", "risk"}, 
assignment.values());
+  }
+
+  @Test
+  void testRejectInvalidValues() {
+    Assertions.assertThrows(IllegalArgumentException.class, 
TagAssignment::ofValues);
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
TagAssignment.ofValues("finance", null));
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
TagAssignment.ofValues("finance", " "));
+  }
+
+  @Test
+  void testEqualsAndHashCode() {
+    TagAssignment assignment1 = TagAssignment.ofValues("finance", "risk");
+    TagAssignment assignment2 = TagAssignment.ofValues("finance", "risk");
+    TagAssignment assignment3 = TagAssignment.ofValues("risk", "finance");
+
+    Assertions.assertEquals(TagAssignment.noValue(), TagAssignment.noValue());
+    Assertions.assertEquals(assignment1, assignment2);
+    Assertions.assertEquals(assignment1.hashCode(), assignment2.hashCode());
+    Assertions.assertNotEquals(assignment1, assignment3);
+  }
+}
diff --git a/api/src/test/java/org/apache/gravitino/tag/TestTagValue.java 
b/api/src/test/java/org/apache/gravitino/tag/TestTagValue.java
new file mode 100644
index 0000000000..d73920f1bf
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/tag/TestTagValue.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.gravitino.tag;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagValue {
+
+  @Test
+  void testValuedTagValue() {
+    TagValue tagValue = TagValue.of("data_domain", "finance");
+
+    Assertions.assertEquals("data_domain", tagValue.name());
+    Assertions.assertEquals("finance", tagValue.value().get());
+  }
+
+  @Test
+  void testNoValueTagValue() {
+    TagValue tagValue = TagValue.noValue("pii");
+
+    Assertions.assertEquals("pii", tagValue.name());
+    Assertions.assertFalse(tagValue.value().isPresent());
+  }
+
+  @Test
+  void testConstructorIsPrivate() {
+    Constructor<?>[] constructors = TagValue.class.getDeclaredConstructors();
+
+    Assertions.assertEquals(1, constructors.length);
+    Assertions.assertTrue(Modifier.isPrivate(constructors[0].getModifiers()));
+    Assertions.assertEquals(0, TagValue.class.getConstructors().length);
+  }
+
+  @Test
+  void testNoJavaBeanAccessors() {
+    Assertions.assertThrows(NoSuchMethodException.class, () -> 
TagValue.class.getMethod("getName"));
+    Assertions.assertThrows(
+        NoSuchMethodException.class, () -> TagValue.class.getMethod("setName", 
String.class));
+    Assertions.assertThrows(
+        NoSuchMethodException.class, () -> 
TagValue.class.getMethod("getValue"));
+    Assertions.assertThrows(
+        NoSuchMethodException.class, () -> 
TagValue.class.getMethod("setValue", String.class));
+  }
+
+  @Test
+  void testRejectInvalidInput() {
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValue.of(null, "finance"));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValue.of(" ", "finance"));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValue.of("tag", null));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValue.of("tag", " "));
+    Assertions.assertThrows(IllegalArgumentException.class, () -> 
TagValue.noValue(" "));
+  }
+
+  @Test
+  void testEqualsAndHashCode() {
+    TagValue tagValue1 = TagValue.of("data_domain", "finance");
+    TagValue tagValue2 = TagValue.of("data_domain", "finance");
+    TagValue tagValue3 = TagValue.of("data_domain", "risk");
+    TagValue tagValue4 = TagValue.noValue("data_domain");
+
+    Assertions.assertEquals(tagValue1, tagValue2);
+    Assertions.assertEquals(tagValue1.hashCode(), tagValue2.hashCode());
+    Assertions.assertNotEquals(tagValue1, tagValue3);
+    Assertions.assertNotEquals(tagValue1, tagValue4);
+  }
+}
diff --git 
a/api/src/test/java/org/apache/gravitino/tag/TestTagValueConstraint.java 
b/api/src/test/java/org/apache/gravitino/tag/TestTagValueConstraint.java
new file mode 100644
index 0000000000..826bf0a1de
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/tag/TestTagValueConstraint.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.apache.gravitino.tag;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestTagValueConstraint {
+
+  @Test
+  void testAnyValueConstraint() {
+    TagValueConstraint constraint = TagValueConstraint.anyValue();
+
+    Assertions.assertEquals(TagValueConstraint.Type.ANY_VALUE, 
constraint.type());
+    Assertions.assertArrayEquals(new String[0], constraint.allowedValues());
+  }
+
+  @Test
+  void testNoValueConstraint() {
+    TagValueConstraint constraint = TagValueConstraint.noValue();
+
+    Assertions.assertEquals(TagValueConstraint.Type.NO_VALUE, 
constraint.type());
+    Assertions.assertArrayEquals(new String[0], constraint.allowedValues());
+  }
+
+  @Test
+  void testAllowedValuesConstraint() {
+    String[] allowedValues = new String[] {"finance", "risk"};
+    TagValueConstraint constraint = 
TagValueConstraint.ofAllowedValues(allowedValues);
+    allowedValues[0] = "changed";
+
+    Assertions.assertEquals(TagValueConstraint.Type.ALLOWED_VALUES, 
constraint.type());
+    Assertions.assertArrayEquals(new String[] {"finance", "risk"}, 
constraint.allowedValues());
+
+    String[] returnedValues = constraint.allowedValues();
+    returnedValues[0] = "changed";
+    Assertions.assertArrayEquals(new String[] {"finance", "risk"}, 
constraint.allowedValues());
+  }
+
+  @Test
+  void testRejectInvalidAllowedValues() {
+    Assertions.assertThrows(IllegalArgumentException.class, 
TagValueConstraint::ofAllowedValues);
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
TagValueConstraint.ofAllowedValues("finance", null));
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> 
TagValueConstraint.ofAllowedValues("finance", " "));
+  }
+
+  @Test
+  void testToStringDifferentiatesConstraintTypes() {
+    Assertions.assertEquals(
+        "TagValueConstraint{type=ANY_VALUE}", 
TagValueConstraint.anyValue().toString());
+    Assertions.assertEquals(
+        "TagValueConstraint{type=NO_VALUE}", 
TagValueConstraint.noValue().toString());
+    Assertions.assertEquals(
+        "TagValueConstraint{type=ALLOWED_VALUES, allowedValues=[finance, 
risk]}",
+        TagValueConstraint.ofAllowedValues("finance", "risk").toString());
+  }
+
+  @Test
+  void testEqualsAndHashCode() {
+    TagValueConstraint constraint1 = 
TagValueConstraint.ofAllowedValues("finance", "risk");
+    TagValueConstraint constraint2 = 
TagValueConstraint.ofAllowedValues("finance", "risk");
+    TagValueConstraint constraint3 = 
TagValueConstraint.ofAllowedValues("risk", "finance");
+
+    Assertions.assertEquals(TagValueConstraint.anyValue(), 
TagValueConstraint.anyValue());
+    Assertions.assertEquals(TagValueConstraint.noValue(), 
TagValueConstraint.noValue());
+    Assertions.assertEquals(constraint1, constraint2);
+    Assertions.assertEquals(constraint1.hashCode(), constraint2.hashCode());
+    Assertions.assertNotEquals(constraint1, constraint3);
+  }
+}

Reply via email to