├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── resources │ │ ├── assets │ │ │ └── fiber_logo.png │ │ └── fabric.mod.json │ └── java │ │ └── io │ │ └── github │ │ └── fablabsmc │ │ └── fablabs │ │ ├── api │ │ └── fiber │ │ │ └── v1 │ │ │ ├── tree │ │ │ ├── Commentable.java │ │ │ ├── HasValue.java │ │ │ ├── ConfigAttribute.java │ │ │ ├── Property.java │ │ │ ├── PropertyMirror.java │ │ │ ├── ConfigBranch.java │ │ │ ├── ConfigLeaf.java │ │ │ ├── ConfigTree.java │ │ │ ├── NodeCollection.java │ │ │ ├── ConfigNode.java │ │ │ └── ConfigQuery.java │ │ │ ├── annotation │ │ │ ├── convention │ │ │ │ ├── package-info.java │ │ │ │ ├── LowercaseConvention.java │ │ │ │ ├── NoNamingConvention.java │ │ │ │ └── SnakeCaseConvention.java │ │ │ ├── collect │ │ │ │ ├── PojoMemberProcessor.java │ │ │ │ └── MemberCollector.java │ │ │ ├── processor │ │ │ │ ├── ParameterizedTypeProcessor.java │ │ │ │ ├── ConfigAnnotationProcessor.java │ │ │ │ ├── LeafAnnotationProcessor.java │ │ │ │ ├── BranchAnnotationProcessor.java │ │ │ │ └── ConstraintAnnotationProcessor.java │ │ │ ├── Listener.java │ │ │ ├── SettingNamingConvention.java │ │ │ ├── Settings.java │ │ │ └── Setting.java │ │ │ ├── exception │ │ │ ├── FiberConversionException.java │ │ │ ├── DuplicateChildException.java │ │ │ ├── IllegalTreeStateException.java │ │ │ ├── FiberException.java │ │ │ ├── MalformedFieldException.java │ │ │ ├── FiberTypeProcessingException.java │ │ │ ├── RuntimeFiberException.java │ │ │ ├── ProcessingMemberException.java │ │ │ ├── ValueDeserializationException.java │ │ │ └── FiberQueryException.java │ │ │ ├── schema │ │ │ ├── type │ │ │ │ ├── ParameterizedSerializableType.java │ │ │ │ ├── PlainSerializableType.java │ │ │ │ ├── ParameterizedTypeImpl.java │ │ │ │ ├── BooleanSerializableType.java │ │ │ │ ├── derived │ │ │ │ │ ├── BooleanConfigType.java │ │ │ │ │ ├── RecordConfigType.java │ │ │ │ │ ├── MapConfigType.java │ │ │ │ │ ├── ListConfigType.java │ │ │ │ │ ├── NumberConfigType.java │ │ │ │ │ ├── StringConfigType.java │ │ │ │ │ ├── EnumConfigType.java │ │ │ │ │ └── ConfigType.java │ │ │ │ ├── TypeCheckResult.java │ │ │ │ ├── EnumSerializableType.java │ │ │ │ ├── RecordSerializableType.java │ │ │ │ ├── StringSerializableType.java │ │ │ │ ├── ListSerializableType.java │ │ │ │ ├── DecimalSerializableType.java │ │ │ │ ├── MapSerializableType.java │ │ │ │ └── SerializableType.java │ │ │ └── SchemaGenerator.java │ │ │ ├── FiberId.java │ │ │ ├── serialization │ │ │ ├── TypeSerializer.java │ │ │ ├── FiberSerialization.java │ │ │ └── JsonTypeSerializer.java │ │ │ └── NodeOperations.java │ │ └── impl │ │ └── fiber │ │ ├── annotation │ │ └── collect │ │ │ ├── MemberCollectorRecursiveImpl.java │ │ │ └── MemberCollectorImpl.java │ │ ├── constraint │ │ ├── BooleanConstraintChecker.java │ │ ├── EnumConstraintChecker.java │ │ ├── ConstraintChecker.java │ │ ├── StringConstraintChecker.java │ │ ├── MapConstraintChecker.java │ │ ├── ListConstraintChecker.java │ │ ├── RecordConstraintChecker.java │ │ └── DecimalConstraintChecker.java │ │ ├── tree │ │ ├── ConfigAttributeImpl.java │ │ ├── PropertyMirrorImpl.java │ │ ├── ConfigLeafImpl.java │ │ ├── IndexedNodeCollection.java │ │ ├── ConfigBranchImpl.java │ │ └── ConfigNodeImpl.java │ │ └── builder │ │ └── ConfigNodeBuilder.java └── test │ └── java │ └── io │ └── github │ └── fablabsmc │ └── fablabs │ ├── impl │ └── fiber │ │ ├── annotation │ │ └── convention │ │ │ ├── NoNamingConventionTest.java │ │ │ └── SnakeCaseConventionTest.java │ │ ├── tree │ │ ├── PropertyMirrorImplTest.java │ │ └── ConfigQueryTest.java │ │ └── builder │ │ └── constraint │ │ └── ConstraintsTest.java │ └── api │ └── fiber │ └── v1 │ ├── schema │ └── type │ │ └── SerializableTypesTest.java │ └── NodeOperationsTest.java ├── .gitignore ├── settings.gradle.kts ├── Jenkinsfile ├── .github └── workflows │ └── gradle.yml ├── gradlew.bat ├── README.md └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | major = 0 2 | minor = 23 3 | patch = 0 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabLabsMC/fiber/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/resources/assets/fiber_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FabLabsMC/fiber/HEAD/src/main/resources/assets/fiber_logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEA 2 | /.idea 3 | /out 4 | /build 5 | /bin 6 | 7 | # Eclipse 8 | /.classpath 9 | /.project 10 | /.settings 11 | 12 | # Gradle 13 | /.gradle 14 | 15 | # Testing 16 | /*.json5 17 | 18 | # Bad OSes 19 | .directory -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven(url = "https://maven.fabricmc.net/") { 4 | name = "Fabric MC" 5 | } 6 | gradlePluginPortal() 7 | } 8 | } 9 | rootProject.name = "fiber" 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/Commentable.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | /** 4 | * Capable of being marked with a comment. 5 | */ 6 | public interface Commentable { 7 | /** 8 | * Returns the comment that was assigned to this class. 9 | * 10 | * @return the comment 11 | */ 12 | String getComment(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/convention/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Classes implementing {@link io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention}. 3 | * 4 | * @see io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention 5 | */ 6 | 7 | @Deprecated 8 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.convention; 9 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/FiberConversionException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | /** 4 | * Thrown when a value cannot be converted to or from a valid serialized form. 5 | */ 6 | public class FiberConversionException extends RuntimeFiberException { 7 | public FiberConversionException(String message) { 8 | super(message); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/DuplicateChildException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | /** 4 | * Thrown when a child node is added to a config tree, but a node 5 | * with the same name already exists in the tree. 6 | */ 7 | public class DuplicateChildException extends IllegalTreeStateException { 8 | public DuplicateChildException(String message) { 9 | super(message); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "fiber", 4 | "version": "${version}", 5 | "name": "fiber", 6 | "description": "A configuration system made for fabric", 7 | "authors": [ 8 | "086", 9 | "Pyrofab" 10 | ], 11 | "contact": { 12 | "sources": "https://github.com/FabLabsMC/fiber" 13 | }, 14 | "license": "Apache License 2.0", 15 | "icon": "assets/fiber_logo.png", 16 | "environment": "*", 17 | "custom": { 18 | "badges": [ "library" ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/IllegalTreeStateException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 4 | 5 | /** 6 | * Signals that a {@link ConfigTree} is not in an appropriate state for 7 | * the requested operation. 8 | */ 9 | public class IllegalTreeStateException extends IllegalStateException { 10 | public IllegalTreeStateException(String message) { 11 | super(message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/convention/LowercaseConvention.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.convention; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention; 4 | 5 | /** 6 | * @deprecated use {@link SettingNamingConvention#LOWERCASE}. 7 | */ 8 | public class LowercaseConvention implements SettingNamingConvention { 9 | @Override 10 | public String name(String name) { 11 | return name.toLowerCase(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/FiberException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | /** 4 | * An exception thrown by fiber when something goes unrecoverably wrong. Unlike {@link RuntimeFiberException}, this exception must be caught. 5 | */ 6 | public class FiberException extends Exception { 7 | public FiberException(String message) { 8 | super(message); 9 | } 10 | 11 | public FiberException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/convention/NoNamingConvention.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.convention; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention; 4 | 5 | /** 6 | * A naming convention that does not modify any names. 7 | * 8 | * @deprecated use {@link SettingNamingConvention#NONE} 9 | */ 10 | public class NoNamingConvention implements SettingNamingConvention { 11 | @Override 12 | public String name(String name) { 13 | return name; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/MalformedFieldException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.AnnotatedSettings; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 5 | 6 | /** 7 | * An exception thrown by {@link AnnotatedSettings AnnotatedSettings} during the conversion of a POJO to a {@link ConfigBranch branch} when a field was not in the expected format. 8 | */ 9 | public class MalformedFieldException extends FiberException { 10 | public MalformedFieldException(String s) { 11 | super(s); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/HasValue.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | /** 6 | * Indicates that this class holds some nullable value. 7 | * 8 | * @param The type of the value being held. 9 | */ 10 | public interface HasValue { 11 | /** 12 | * Returns the value being held. 13 | * 14 | * @return the value 15 | */ 16 | @Nonnull 17 | T getValue(); 18 | 19 | /** 20 | * Returns the class of the type of the value being held. 21 | * 22 | * @return the class of the type of the value 23 | */ 24 | Class getType(); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/FiberTypeProcessingException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 4 | 5 | /** 6 | * Thrown when an annotated POJO cannot be processed into a valid 7 | * {@link ConfigTree}. 8 | * 9 | * @see io.github.fablabsmc.fablabs.api.fiber.v1.annotation.AnnotatedSettings 10 | */ 11 | public class FiberTypeProcessingException extends FiberException { 12 | public FiberTypeProcessingException(String message) { 13 | super(message); 14 | } 15 | 16 | public FiberTypeProcessingException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/RuntimeFiberException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | /** 4 | * An exception thrown by fiber when something goes unrecoverably wrong. 5 | * 6 | *

Unlike {@link FiberException}, this exception doesn't have to be caught. It happens only with grave mistakes like putting numerical constraints on non-numeric values. 7 | */ 8 | public class RuntimeFiberException extends RuntimeException { 9 | public RuntimeFiberException() { 10 | super(); 11 | } 12 | 13 | public RuntimeFiberException(String s) { 14 | super(s); 15 | } 16 | 17 | public RuntimeFiberException(String s, Throwable throwable) { 18 | super(s, throwable); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/ProcessingMemberException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | import java.lang.reflect.Member; 4 | 5 | /** 6 | * Thrown when an error occurs within annotated member processing. 7 | */ 8 | public class ProcessingMemberException extends FiberException { 9 | private final Member member; 10 | 11 | public ProcessingMemberException(String message, Member member) { 12 | super(message); 13 | this.member = member; 14 | } 15 | 16 | public ProcessingMemberException(String message, Throwable cause, Member member) { 17 | super(message, cause); 18 | this.member = member; 19 | } 20 | 21 | public Member getMember() { 22 | return this.member; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/convention/SnakeCaseConvention.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.convention; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention; 4 | 5 | /** 6 | * A naming convention that converts java-styled {@code lowerCamelCase} names and {@code Proper case} names to {@code snake_case}. 7 | * 8 | * @see SnakeCaseConvention#name(String) 9 | * @deprecated use {@link SettingNamingConvention#SNAKE_CASE}. 10 | */ 11 | public class SnakeCaseConvention implements SettingNamingConvention { 12 | @Override 13 | public String name(String name) { 14 | return name.replaceAll("(?!^)[ _]*([A-Z])", "_$1").toLowerCase().replace(' ', '_'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/impl/fiber/annotation/convention/NoNamingConventionTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.annotation.convention; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention; 6 | import org.junit.jupiter.api.Test; 7 | 8 | class NoNamingConventionTest { 9 | private static String[] STRINGS = { 10 | "anything", 11 | "you", 12 | "put", 13 | "here", 14 | "will", 15 | "just", 16 | "stay", 17 | "the", 18 | "same", 19 | "." 20 | }; 21 | 22 | @Test 23 | void name() { 24 | for (String string : STRINGS) { 25 | assertEquals(string, SettingNamingConvention.NONE.name(string)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/collect/PojoMemberProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.collect; 2 | 3 | import java.lang.reflect.Field; 4 | import java.lang.reflect.Method; 5 | 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ProcessingMemberException; 7 | 8 | /** 9 | * Implementors consume and process collected POJO members. 10 | */ 11 | public interface PojoMemberProcessor { 12 | void processListenerMethod(Object pojo, Method method, String name); 13 | 14 | void processListenerField(Object pojo, Field field, String name); 15 | 16 | void processSetting(Object pojo, Field setting) throws ProcessingMemberException; 17 | 18 | void processGroup(Object pojo, Field group) throws ProcessingMemberException; 19 | } 20 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | stages { 4 | stage('Build') { 5 | steps { 6 | sh "rm -rf build/libs/" 7 | sh "chmod +x gradlew" 8 | sh "./gradlew clean build --refresh-dependencies --full-stacktrace" 9 | } 10 | } 11 | 12 | stage('Check') { 13 | steps { 14 | sh "./gradlew check" 15 | } 16 | } 17 | 18 | stage('Archive artifacts') { 19 | when { 20 | branch 'master' 21 | } 22 | steps { 23 | sh "./gradlew publish" 24 | } 25 | } 26 | 27 | stage("counter") { 28 | steps { 29 | sh "./gradlew buildnumberIncrement" 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/annotation/collect/MemberCollectorRecursiveImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.annotation.collect; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.collect.PojoMemberProcessor; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ProcessingMemberException; 5 | 6 | public class MemberCollectorRecursiveImpl extends MemberCollectorImpl { 7 | public MemberCollectorRecursiveImpl(boolean onlyAnnotated) { 8 | super(onlyAnnotated); 9 | } 10 | 11 | @Override 12 | public

void collect(P pojo, Class clazz, PojoMemberProcessor processor) throws ProcessingMemberException { 13 | super.collect(pojo, clazz, processor); 14 | 15 | if (clazz.getSuperclass() != null) { 16 | this.collect(pojo, clazz.getSuperclass(), processor); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | - name: Check with Gradle 28 | run: ./gradlew check 29 | - name: Build javadocs with Gradle 30 | run: ./gradlew javadoc 31 | -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/impl/fiber/annotation/convention/SnakeCaseConventionTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.annotation.convention; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.SettingNamingConvention; 6 | import org.junit.jupiter.api.DisplayName; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class SnakeCaseConventionTest { 10 | private static final String[] STRINGS = { 11 | "Hello world", 12 | "helloWorld", 13 | "HelloWorld", 14 | "Hello World", 15 | "Hello_World", 16 | "hello_world" 17 | }; 18 | 19 | private static final String EXPECTED = "hello_world"; 20 | 21 | @Test 22 | @DisplayName("Name conversions") 23 | void testName() { 24 | for (String s : STRINGS) { 25 | assertEquals(EXPECTED, SettingNamingConvention.SNAKE_CASE.name(s), "Converting " + s + " to " + EXPECTED); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/ParameterizedSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | 6 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.ConstraintChecker; 7 | 8 | /** 9 | * A {@link SerializableType} that represents a {@link ParameterizedType}. 10 | */ 11 | public abstract class ParameterizedSerializableType extends SerializableType { 12 | ParameterizedSerializableType(Class platformType, ConstraintChecker> checker) { 13 | super(platformType, checker); 14 | } 15 | 16 | /** 17 | * The {@link ParameterizedType} used to represent values of this type. 18 | */ 19 | public abstract ParameterizedType getParameterizedType(); 20 | 21 | @Override 22 | public Type getGenericPlatformType() { 23 | return this.getParameterizedType(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/BooleanConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.BooleanSerializableType; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 5 | 6 | public class BooleanConstraintChecker extends ConstraintChecker { 7 | private static final BooleanConstraintChecker INSTANCE = new BooleanConstraintChecker(); 8 | 9 | public static BooleanConstraintChecker instance() { 10 | return INSTANCE; 11 | } 12 | 13 | private BooleanConstraintChecker() { 14 | } 15 | 16 | @Override 17 | public TypeCheckResult test(BooleanSerializableType cfg, Boolean value) { 18 | return TypeCheckResult.successful(value); 19 | } 20 | 21 | @Override 22 | public boolean comprehends(BooleanSerializableType cfg, BooleanSerializableType cfg2) { 23 | return true; // all boolean types are equal 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/ConfigAttribute.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.FiberId; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 8 | import io.github.fablabsmc.fablabs.impl.fiber.tree.ConfigAttributeImpl; 9 | 10 | public interface ConfigAttribute extends Property { 11 | static ConfigAttribute create(FiberId id, ConfigType type, @Nonnull R defaultValue) { 12 | return create(id, type.getSerializedType(), type.toSerializedType(defaultValue)); 13 | } 14 | 15 | static ConfigAttribute create(FiberId id, SerializableType type, @Nonnull A defaultValue) { 16 | return new ConfigAttributeImpl<>(id, type, defaultValue); 17 | } 18 | 19 | @Override 20 | default Class getType() { 21 | return this.getConfigType().getErasedPlatformType(); 22 | } 23 | 24 | SerializableType getConfigType(); 25 | 26 | FiberId getIdentifier(); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/PlainSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.Type; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.ConstraintChecker; 8 | 9 | /** 10 | * A {@link SerializableType} that represents a plain (non-parameterized) type. 11 | * 12 | * @param The Java platform type. 13 | */ 14 | public abstract class PlainSerializableType extends SerializableType { 15 | PlainSerializableType(Class platformType, ConstraintChecker> checker) { 16 | super(platformType, checker); 17 | } 18 | 19 | @SuppressWarnings("unchecked") 20 | @Override 21 | public Class getErasedPlatformType() { 22 | // this unchecked cast is safe when T is not a parameterized type. 23 | return (Class) super.getErasedPlatformType(); 24 | } 25 | 26 | @Override 27 | public Type getGenericPlatformType() { 28 | return this.getErasedPlatformType(); 29 | } 30 | 31 | @Override 32 | public T cast(@Nonnull Object value) { 33 | return this.getErasedPlatformType().cast(value); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/ValueDeserializationException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | import javax.annotation.Nullable; 4 | 5 | /** 6 | * Thrown when a {@link io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer} 7 | * cannot deserialize a value. 8 | */ 9 | public class ValueDeserializationException extends FiberException { 10 | @Nullable 11 | private final Object value; 12 | private final Class targetType; 13 | 14 | public ValueDeserializationException(@Nullable Object value, Class targetType, String message) { 15 | super(message); 16 | this.value = value; 17 | this.targetType = targetType; 18 | } 19 | 20 | public ValueDeserializationException(@Nullable Object value, Class targetType, String message, Throwable cause) { 21 | super(message, cause); 22 | this.value = value; 23 | this.targetType = targetType; 24 | } 25 | 26 | /** 27 | * The value that could not be deserialized. 28 | */ 29 | @Nullable 30 | public Object getValue() { 31 | return value; 32 | } 33 | 34 | /** 35 | * The target type that could not be deserialized to. 36 | */ 37 | public Class getTargetType() { 38 | return targetType; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/collect/MemberCollector.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.collect; 2 | 3 | import java.lang.annotation.ElementType; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ProcessingMemberException; 6 | 7 | /** 8 | * Implementors of this interface search for and collect config settings and metadata 9 | * within POJO instances. 10 | */ 11 | public interface MemberCollector { 12 | /** 13 | * Tries to find all listeners, settings, and groups in a specified POJO, 14 | * passing them on to the given {@code processor}. 15 | * 16 | *

This method performs all logic to exclude members 17 | * based on their properties, such as annotations or modifiers. 18 | * 19 | *

This method targets {@linkplain ElementType#FIELD}. 20 | * 21 | * @param pojo the instance of the POJO to scan 22 | * @param clazz the class to scan 23 | * @param processor the member processor 24 | * @param

the generic pojo type 25 | * @throws ProcessingMemberException if something went wrong while processing a member 26 | */ 27 |

void collect(P pojo, Class clazz, PojoMemberProcessor processor) throws ProcessingMemberException; 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/processor/ParameterizedTypeProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.ListSerializableType; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 6 | 7 | /** 8 | * A generic config type processor that can output different {@link SerializableType}s 9 | * for a single class, based on generic type parameters. 10 | * 11 | *

Example implementation for a {@code List} type processor: 12 | *

{@code (typeArguments) -> ConfigTypes.makeList(typeArguments[0])}
13 | * 14 | * @param the bare type being processed, eg. {@code List}. 15 | * @see ListSerializableType 16 | */ 17 | @FunctionalInterface 18 | public interface ParameterizedTypeProcessor { 19 | /** 20 | * Produces usable config type information using the given {@code typeArguments}. 21 | * 22 | * @param typeArguments the generic type parameters 23 | * @return a ConfigType representing the parameterized type 24 | */ 25 | ConfigType process(ConfigType[] typeArguments); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/tree/ConfigAttributeImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.FiberId; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigAttribute; 10 | 11 | public class ConfigAttributeImpl implements ConfigAttribute { 12 | private final FiberId identifier; 13 | private final SerializableType type; 14 | @Nonnull 15 | private T value; 16 | 17 | public ConfigAttributeImpl(FiberId identifier, SerializableType type, @Nonnull T value) { 18 | this.identifier = identifier; 19 | this.type = type; 20 | this.value = Objects.requireNonNull(value); 21 | } 22 | 23 | @Override 24 | public boolean setValue(@Nonnull T value) { 25 | this.value = Objects.requireNonNull(value); 26 | return true; 27 | } 28 | 29 | @Override 30 | @Nonnull 31 | public T getValue() { 32 | return this.value; 33 | } 34 | 35 | @Override 36 | public SerializableType getConfigType() { 37 | return this.type; 38 | } 39 | 40 | @Override 41 | public FiberId getIdentifier() { 42 | return this.identifier; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/EnumConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import java.util.Locale; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.EnumSerializableType; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 7 | 8 | public final class EnumConstraintChecker extends ConstraintChecker { 9 | private static final EnumConstraintChecker INSTANCE = new EnumConstraintChecker(); 10 | 11 | public static EnumConstraintChecker instance() { 12 | return INSTANCE; 13 | } 14 | 15 | private EnumConstraintChecker() { 16 | } 17 | 18 | @Override 19 | public TypeCheckResult test(EnumSerializableType cfg, String value) { 20 | if (cfg.getValidValues().contains(value)) { 21 | return TypeCheckResult.successful(value); 22 | } 23 | 24 | String corrected = value.toUpperCase(Locale.ROOT); 25 | 26 | if (cfg.getValidValues().contains(corrected)) { 27 | return TypeCheckResult.failed(corrected); 28 | } 29 | 30 | return TypeCheckResult.unrecoverable(); 31 | } 32 | 33 | @Override 34 | public boolean comprehends(EnumSerializableType cfg, EnumSerializableType cfg2) { 35 | return cfg.getValidValues().containsAll(cfg2.getValidValues()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/FiberId.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.annotation.Nonnull; 6 | 7 | /** 8 | * A namespaced string, representing an identifier without 9 | * character restrictions. 10 | * 11 | *

A {@code FiberId} contains two names: the domain and the name. The domain and name 12 | * may be represented as a single string by separating them with a colon ({@code :}). 13 | */ 14 | public final class FiberId { 15 | private final String domain; 16 | private final String name; 17 | 18 | public FiberId(@Nonnull String domain, @Nonnull String name) { 19 | this.domain = domain; 20 | this.name = name; 21 | } 22 | 23 | public String getName() { 24 | return name; 25 | } 26 | 27 | public String getDomain() { 28 | return domain; 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return getDomain() + ":" + getName(); 34 | } 35 | 36 | @Override 37 | public boolean equals(Object o) { 38 | if (this == o) { 39 | return true; 40 | } 41 | 42 | if (o == null || getClass() != o.getClass()) { 43 | return false; 44 | } 45 | 46 | FiberId fiberId = (FiberId) o; 47 | return domain.equals(fiberId.domain) && name.equals(fiberId.name); 48 | } 49 | 50 | @Override 51 | public int hashCode() { 52 | return Objects.hash(domain, name); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/processor/ConfigAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.reflect.AnnotatedElement; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.AnnotatedSettings; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 9 | 10 | /** 11 | * An annotation processor for config fields. 12 | * 13 | *

Annotations made for this type of processor should 14 | * specifically target {@link ElementType#FIELD}. 15 | * 16 | * @param the type of annotations processed 17 | * @param the type of builders configured 18 | * @see AnnotatedSettings 19 | */ 20 | public interface ConfigAnnotationProcessor { 21 | /** 22 | * Called for every field that has an annotation of type {@code A}. 23 | * 24 | * @param annotation the annotation present on the {@code field} 25 | * @param field a field declared in {@code pojo}'s class 26 | * @param pojo the plain old java object being processed 27 | * @param setting the builder being configured 28 | * @see AnnotatedSettings#applyToNode(ConfigTree, Object) 29 | */ 30 | void apply(A annotation, E field, Object pojo, C setting); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/Property.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | /** 6 | * Implementing this interface means that this class has a nullable value 7 | * which can be mutated using the {@link Property#setValue(Object) setValue} method. 8 | * 9 | * @param the type of value this property holds 10 | * @see Property#setValue(Object) 11 | */ 12 | public interface Property extends HasValue { 13 | /** 14 | * Sets the value of this property. 15 | * 16 | *

This can fail and return {@code false} if, for example, the value did not satisfy the constraints of the property. 17 | * This operation can still succeed with a {@linkplain #accepts(Object) rejected} value, 18 | * if eg. a corrected value can be found. 19 | * 20 | * @param value the new value this property should receive 21 | * @return {@code true} if this property changed as a result of the call 22 | * @see #accepts(Object) 23 | */ 24 | boolean setValue(@Nonnull T value); 25 | 26 | /** 27 | * Returns {@code true} if this property can be set to the given value. 28 | * 29 | * @param value the value to check 30 | * @return {@code true} if this property accepts the given value, {@code false} otherwise. 31 | * @see #setValue(Object) 32 | */ 33 | default boolean accepts(@Nonnull T value) { 34 | return true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/Listener.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Indicates that this field or method is a listener listening to changes of another field's value. 10 | */ 11 | @Target({ElementType.FIELD, ElementType.METHOD}) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface Listener { 14 | /** 15 | * The name of the setting this listener is listening for. 16 | * 17 | *

Note that this must be equal to the resolved name of the setting: if you have specified a naming convention for your settings, or you have set a custom name for the setting, this value must be equal to the name of the setting you're listening for after that naming convention was applied! 18 | * 19 | *

For example, if you are using {@link SettingNamingConvention#SNAKE_CASE}: 20 | *

21 | 	 * class MySettings {
22 | 	 *     private int fooBar = 5;
23 | 	 *
24 | 	 *     @Listener("foo_bar") // foo_bar not fooBar
25 | 	 *     public void fooBarListener(int newValue) {
26 | 	 *         System.out.println("Changed to " + newValue);
27 | 	 *     }
28 | 	 * }
29 | 	 * 
30 | * 31 | * @return the name of the setting this listener listens for 32 | */ 33 | String value(); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/ParameterizedTypeImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.lang.reflect.Type; 5 | import java.util.Arrays; 6 | 7 | /** 8 | * A {@link ParameterizedType} implementation used by {@link ParameterizedSerializableType}. 9 | */ 10 | final class ParameterizedTypeImpl implements ParameterizedType { 11 | private final Class rawType; 12 | private final Type[] typeArguments; 13 | 14 | ParameterizedTypeImpl(Class rawType, Type... typeArguments) { 15 | this.rawType = rawType; 16 | this.typeArguments = typeArguments; 17 | } 18 | 19 | @Override 20 | public Type[] getActualTypeArguments() { 21 | return this.typeArguments.clone(); 22 | } 23 | 24 | @Override 25 | public Type getRawType() { 26 | return this.rawType; 27 | } 28 | 29 | @Override 30 | public Type getOwnerType() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | 39 | ParameterizedTypeImpl that = (ParameterizedTypeImpl) o; 40 | 41 | if (!rawType.equals(that.rawType)) return false; 42 | return Arrays.equals(typeArguments, that.typeArguments); 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | int result = rawType.hashCode(); 48 | result = 31 * result + Arrays.hashCode(typeArguments); 49 | return result; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/SettingNamingConvention.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation; 2 | 3 | import java.util.Locale; 4 | import java.util.regex.Pattern; 5 | 6 | /** 7 | * A setting naming convention decides how a setting is named based upon the variable it was created from. 8 | * 9 | *

Java fields are usually {@code lowerCamelCase}, while developers might want their configuration to use {@code snake_case} or {@code lowercase}. 10 | * Classes implementing this interface are required to make the conversion from {@code lowerCamelCase} to another format. 11 | */ 12 | @FunctionalInterface 13 | public interface SettingNamingConvention { 14 | /** 15 | * A naming convention that converts all characters to lowercase. 16 | */ 17 | SettingNamingConvention LOWERCASE = name -> name.toLowerCase(Locale.ROOT); 18 | 19 | /** 20 | * A naming convention that does not modify any names. 21 | */ 22 | SettingNamingConvention NONE = name -> name; 23 | 24 | Pattern CAMEL_WORD_START = Pattern.compile("(?!^)[ _]*([A-Z])"); 25 | 26 | /** 27 | * A naming convention that converts java-styled {@code lowerCamelCase} names and {@code Proper case} names to {@code snake_case}. 28 | */ 29 | SettingNamingConvention SNAKE_CASE = name -> CAMEL_WORD_START.matcher(name).replaceAll("_$1").replace(' ', '_').toLowerCase(Locale.ROOT); 30 | 31 | /** 32 | * For the given {@code lowerCamelCase} name, returns a name using the convention 33 | * defined by this object. 34 | * 35 | * @param name The name, in lower camel case. 36 | * @return The name, formatted according to the convention defined by this object. 37 | */ 38 | String name(String name); 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/BooleanSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 6 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.BooleanConstraintChecker; 7 | 8 | /** 9 | * The {@link SerializableType} for {@code boolean} values. 10 | */ 11 | public final class BooleanSerializableType extends PlainSerializableType { 12 | public static final BooleanSerializableType BOOLEAN = new BooleanSerializableType(); 13 | 14 | private BooleanSerializableType() { 15 | super(Boolean.class, BooleanConstraintChecker.instance()); 16 | } 17 | 18 | @Override 19 | public void serialize(TypeSerializer serializer, S target) { 20 | serializer.serialize(this, target); 21 | } 22 | 23 | @Override 24 | public S serializeValue(Boolean value, ValueSerializer serializer) { 25 | return serializer.serializeBoolean(value, this); 26 | } 27 | 28 | @Override 29 | public Boolean deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 30 | return serializer.deserializeBoolean(elem, this); 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | return o instanceof BooleanSerializableType; 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return 1337; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return BooleanSerializableType.class.getSimpleName(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/processor/LeafAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.reflect.Field; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.AnnotatedSettings; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.Setting; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigLeafBuilder; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 11 | 12 | /** 13 | * An annotation processor for config fields holding values. 14 | * 15 | *

Annotations made to be handled by these processors should 16 | * specifically target {@link ElementType#FIELD}. 17 | * 18 | * @param the type of annotations processed 19 | * @see AnnotatedSettings.Builder#registerSettingProcessor(Class, LeafAnnotationProcessor) 20 | */ 21 | @FunctionalInterface 22 | public interface LeafAnnotationProcessor extends ConfigAnnotationProcessor> { 23 | /** 24 | * Called for every field that has an annotation of type {@code A} 25 | * and is not annotated with {@link Setting.Group @Setting.Group} or {@link Setting#ignore()}. 26 | * 27 | * @param annotation the annotation present on the {@code field} 28 | * @param field a field declared in {@code pojo}'s class 29 | * @param pojo the plain old java object being processed 30 | * @param builder the builder being configured 31 | * @see AnnotatedSettings#applyToNode(ConfigTree, Object) 32 | */ 33 | @Override 34 | void apply(A annotation, Field field, Object pojo, ConfigLeafBuilder builder); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/BooleanConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.function.Function; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.BooleanSerializableType; 9 | 10 | /** 11 | * A config type for boolean values. 12 | * 13 | * @param The runtime type of the underlying boolean values. 14 | */ 15 | public final class BooleanConfigType extends ConfigType { 16 | public BooleanConfigType(Class actualType, Function deserializer, Function serializer) { 17 | super(BooleanSerializableType.BOOLEAN, actualType, deserializer, serializer); 18 | } 19 | 20 | @Override 21 | public BooleanConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 22 | @SuppressWarnings("unchecked") Class c = (Class) runtimeType; 23 | return new BooleanConfigType<>(c, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 24 | } 25 | 26 | @Override 27 | public ConfigType withType(BooleanSerializableType newSpec) { 28 | // no narrowing possible for booleans 29 | return this; 30 | } 31 | 32 | @Override 33 | public BooleanConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 34 | return processor.processBoolean(this, annotation, annotated); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/ConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 6 | 7 | /** 8 | * Checks the validity of values based on a {@link SerializableType}'s constraints. 9 | * 10 | * @param the type of values this constraint checks 11 | * @see ConfigLeaf 12 | * @see SerializableType 13 | */ 14 | public abstract class ConstraintChecker> { 15 | ConstraintChecker() { 16 | } 17 | 18 | /** 19 | * Tests a value against this {@code Constraint}. 20 | * 21 | *

This method may provide a corrected value that can be used 22 | * if the input value is invalid. 23 | * 24 | * @param cfg the type configuration to test against 25 | * @param value the value 26 | * @return {@code true} if {@code value} satisfies the constraint 27 | */ 28 | public abstract TypeCheckResult test(T cfg, V value); 29 | 30 | /** 31 | * Returns {@code true} if {@code cfg} comprehends {@code cfg2}. 32 | * 33 | *

A type configuration comprehends another if it accepts every 34 | * value that the other does. 35 | *

forall x, cfg2.accepts(x) => cfg.accepts(x)
36 | * 37 | * @param cfg the tested comprehensive type configuration 38 | * @param cfg2 the tested comprehended type configuration 39 | * @return {@code true} if {@code cfg} comprehends {@code cfg2}, 40 | * otherwise {@code false}. 41 | * @see SerializableType#isAssignableFrom(SerializableType) 42 | */ 43 | public abstract boolean comprehends(T cfg, T cfg2); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/processor/BranchAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.reflect.Field; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.AnnotatedSettings; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.Setting.Group; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigTreeBuilder; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 11 | 12 | /** 13 | * An annotation processor for fields representing config groups. 14 | * 15 | *

In effect, this is called for every field in a config POJO 16 | * class that is annotated with {@link Group}. 17 | * 18 | *

Annotations made for these processors should 19 | * specifically target {@link ElementType#FIELD}. 20 | * 21 | * @param the type of annotations processed 22 | * @see AnnotatedSettings.Builder#registerGroupProcessor(Class, BranchAnnotationProcessor) 23 | */ 24 | @FunctionalInterface 25 | public interface BranchAnnotationProcessor extends ConfigAnnotationProcessor { 26 | /** 27 | * Called for every field that has an annotation of type {@code A} and is annotated with {@link Group @Group}. 28 | * 29 | * @param annotation the annotation present on the {@code field} 30 | * @param field a field declared in {@code pojo}'s class 31 | * @param pojo the plain old java object being processed 32 | * @param builder the builder being configured 33 | * @see AnnotatedSettings#applyToNode(ConfigTree, Object) 34 | */ 35 | @Override 36 | void apply(A annotation, Field field, Object pojo, ConfigTreeBuilder builder); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/StringConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.StringSerializableType; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 7 | 8 | /** 9 | * Checks validity of serialized strings based on a {@code StringConfigType}'s length range and pattern. 10 | */ 11 | public final class StringConstraintChecker extends ConstraintChecker { 12 | private static final StringConstraintChecker INSTANCE = new StringConstraintChecker(); 13 | 14 | public static StringConstraintChecker instance() { 15 | return INSTANCE; 16 | } 17 | 18 | private StringConstraintChecker() { 19 | } 20 | 21 | @Override 22 | public TypeCheckResult test(StringSerializableType cfg, String value) { 23 | if (value.length() < cfg.getMinLength()) { 24 | return TypeCheckResult.unrecoverable(); 25 | } 26 | 27 | if (value.length() > cfg.getMaxLength()) { 28 | return TypeCheckResult.unrecoverable(); 29 | } 30 | 31 | Pattern pattern = cfg.getPattern(); 32 | 33 | if (pattern != null && !pattern.matcher(value).matches()) { 34 | return TypeCheckResult.unrecoverable(); 35 | } 36 | 37 | return TypeCheckResult.successful(value); 38 | } 39 | 40 | @Override 41 | public boolean comprehends(StringSerializableType cfg, StringSerializableType cfg2) { 42 | if (cfg.getMinLength() > cfg2.getMinLength()) { 43 | return false; 44 | } 45 | 46 | if (cfg.getMaxLength() < cfg2.getMaxLength()) { 47 | return false; 48 | } 49 | 50 | // TODO detect if this pattern matches every string the other pattern can? 51 | return cfg.getPattern() == null || cfg.getPattern().equals(cfg2.getPattern()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/Settings.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.convention.NoNamingConvention; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 10 | 11 | /** 12 | * Indicates a type represents a structure in a configuration file. 13 | * 14 | *

While it not necessary to use this annotation to generate {@link ConfigTree}s from a POJO class, 15 | * it can be used to specify other metadata. 16 | * 17 | * @see Settings#onlyAnnotated() 18 | */ 19 | @Target({ElementType.TYPE}) 20 | @Retention(RetentionPolicy.RUNTIME) 21 | public @interface Settings { 22 | /** 23 | * Specifies whether or not all fields in this class should be serialised, or only those annotated with {@link Setting}. 24 | * 25 | *

If you want to exclude one field without having to mark all others with the {@link Setting} annotation, the field can be marked as {@code transient} instead. 26 | * All transient fields are ignored by default. 27 | * 28 | * @return whether or not only annotated fields should be serialised 29 | * @see AnnotatedSettings.Builder#collectOnlyAnnotatedMembers() 30 | */ 31 | boolean onlyAnnotated() default false; 32 | 33 | /** 34 | * Returns the naming convention used for (re)naming the fields in this class during serialisation. 35 | * 36 | * @return the {@link SettingNamingConvention naming convention} for this class 37 | * @deprecated use {@link AnnotatedSettings.Builder#useNamingConvention(SettingNamingConvention)} 38 | */ 39 | @Deprecated 40 | Class namingConvention() default NoNamingConvention.class; 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/PropertyMirror.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 4 | import io.github.fablabsmc.fablabs.impl.fiber.tree.PropertyMirrorImpl; 5 | 6 | /** 7 | * A {@code Property} that delegates all operations to another. 8 | * 9 | *

This can be used in conjunction with config builders to 10 | * easily setup a configuration without reflection. For example: 11 | *

{@code
12 |  * public final PropertyMirror diamondsDropped = new PropertyMirror<>();
13 |  *
14 |  * private final Node config = ConfigNode.builder()
15 |  *     .beginValue("diamondsDropped", Integer.class)
16 |  * 		   .beginConstraints().atLeast(1).finishConstraints()
17 |  *     .finishValue(diamondsDropped::mirror)
18 |  *     .build();
19 |  * }
20 | * 21 | * @param the type of value this property mirrors 22 | */ 23 | public interface PropertyMirror extends Property { 24 | /** 25 | * Creates a new {@link PropertyMirror} that can mirror values of the given {@link ConfigType}. 26 | * 27 | * @param converter The ConfigType of the mirrored values. 28 | * @param The type of the mirrored values. 29 | */ 30 | static PropertyMirror create(ConfigType converter) { 31 | return new PropertyMirrorImpl<>(converter); 32 | } 33 | 34 | /** 35 | * Sets a property to mirror. 36 | * 37 | *

After calling this method with a valid delegate, 38 | * every property method will redirect to {@code delegate}. 39 | * 40 | * @param delegate a property to mirror 41 | */ 42 | void mirror(Property delegate); 43 | 44 | /** 45 | * Returns the mirrored property. 46 | */ 47 | Property getMirrored(); 48 | 49 | /** 50 | * Returns the {@link ConfigType} of the mirrored property. 51 | */ 52 | ConfigType getMirroredType(); 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/impl/fiber/tree/PropertyMirrorImplTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigTypes; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigAttribute; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.PropertyMirror; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class PropertyMirrorImplTest { 14 | // see https://github.com/FabLabsMC/fiber/issues/63 15 | @Test 16 | @DisplayName("Passively invalidated values are correct") 17 | public void testPassiveInvalidation() { 18 | PropertyMirror mirror = PropertyMirror.create(ConfigTypes.BOOLEAN); 19 | ConfigTree.builder() 20 | .beginValue("mirrored", ConfigTypes.BOOLEAN, false) 21 | .finishValue(mirror::mirror) 22 | .build(); 23 | 24 | assertFalse(mirror.getValue()); 25 | 26 | assertTrue(mirror.setValue(true)); 27 | assertTrue(mirror.getValue()); 28 | 29 | assertTrue(mirror.setValue(true)); 30 | assertTrue(mirror.getValue()); 31 | 32 | assertTrue(mirror.setValue(false)); 33 | assertFalse(mirror.getValue()); 34 | } 35 | 36 | @Test 37 | @DisplayName("Actively invalidated values are correct") 38 | public void testActiveInvalidation() { 39 | // Attributes aren't ConfigLeafs, so they will use active invalidation. 40 | ConfigAttribute attribute = ConfigAttribute.create(null, ConfigTypes.BOOLEAN, false); 41 | PropertyMirror mirror = PropertyMirror.create(ConfigTypes.BOOLEAN); 42 | mirror.mirror(attribute); 43 | 44 | assertFalse(mirror.getValue()); 45 | 46 | assertTrue(mirror.setValue(true)); 47 | assertTrue(mirror.getValue()); 48 | 49 | assertTrue(mirror.setValue(true)); 50 | assertTrue(mirror.getValue()); 51 | 52 | assertTrue(mirror.setValue(false)); 53 | assertFalse(mirror.getValue()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/ConfigBranch.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigTreeBuilder; 7 | 8 | /** 9 | * A node that can hold any amount of children 10 | * 11 | *

A branch may represent an entire config tree, 12 | * or a subtree grouping further configuration items. 13 | * 14 | * @see ConfigTreeBuilder 15 | */ 16 | public interface ConfigBranch extends ConfigTree, ConfigNode, Commentable { 17 | /** 18 | * Returns this node's name. 19 | * 20 | *

If this node is the root of a config tree, it may not have a name. 21 | * In this case, this method returns {@code null}. 22 | * 23 | * @return this node's name, or {@code null} if it does not have any. 24 | */ 25 | @Nullable 26 | @Override 27 | String getName(); 28 | 29 | /** 30 | * Tries to find a child in this group by name. If a child is found, it will be returned. 31 | * 32 | *

The node returned for a given {@code name} is always the same. 33 | * 34 | * @param name The name of the child to look for 35 | * @return the child if found, otherwise {@code null} 36 | */ 37 | @Nullable 38 | @Override 39 | ConfigNode lookup(String name); 40 | 41 | /** 42 | * Returns a collection of this branch's children. 43 | * 44 | *

The returned collection is guaranteed not to have two nodes with the same name. 45 | * 46 | * @return the set of children 47 | */ 48 | @Nonnull 49 | @Override 50 | NodeCollection getItems(); 51 | 52 | /** 53 | * Returns {@code true} if this node should be serialized separately from its parent. 54 | * 55 | *

If a node is serialized separately, it should not appear in the serialized representation of 56 | * its ancestors. This property should be ignored if this node is the 57 | * root of the currently serialised tree. 58 | * 59 | * @return {@code true} if this node should be serialized separately, and {@code false} otherwise 60 | */ 61 | default boolean isSerializedSeparately() { 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/SchemaGenerator.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema; 2 | 3 | import blue.endless.jankson.JsonElement; 4 | import blue.endless.jankson.JsonObject; 5 | import blue.endless.jankson.JsonPrimitive; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.JsonTypeSerializer; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 12 | 13 | public class SchemaGenerator { 14 | private final ValueSerializer serializer; 15 | private final JsonTypeSerializer typeSerializer; 16 | 17 | public SchemaGenerator(ValueSerializer serializer) { 18 | this.serializer = serializer; 19 | this.typeSerializer = new JsonTypeSerializer(); 20 | } 21 | 22 | public JsonObject createSchema(ConfigTree tree) { 23 | JsonObject object = new JsonObject(); 24 | 25 | for (ConfigNode item : tree.getItems()) { // TODO: Maybe allow for custom schema deserializers? / generic metadata 26 | if (item instanceof ConfigBranch) { 27 | object.put(item.getName(), this.createSchema((ConfigTree) item)); 28 | } else if (item instanceof ConfigLeaf) { 29 | object.put(item.getName(), this.createSchema((ConfigLeaf) item)); 30 | } 31 | 32 | // TODO attributes 33 | } 34 | 35 | return object; 36 | } 37 | 38 | private JsonObject createSchema(ConfigLeaf item) { 39 | JsonObject object = new JsonObject(); 40 | JsonObject type = new JsonObject(); 41 | this.typeSerializer.serializeType(item.getConfigType(), type); 42 | object.put("type", type); 43 | 44 | if (item.getComment() != null) { 45 | object.put("comment", new JsonPrimitive(item.getComment())); 46 | } 47 | 48 | if (item.getDefaultValue() != null) { 49 | Object o = item.getDefaultValue(); 50 | object.put("defaultValue", item.getConfigType().serializeValue(item.getDefaultValue(), this.serializer)); 51 | } 52 | 53 | return object; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/serialization/TypeSerializer.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.serialization; 2 | 3 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.BooleanSerializableType; 4 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.DecimalSerializableType; 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.EnumSerializableType; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.ListSerializableType; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.MapSerializableType; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.RecordSerializableType; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.StringSerializableType; 11 | 12 | /** 13 | * A serializer visitor for {@link SerializableType} schemas. 14 | * 15 | * @param The type of the serialized form. 16 | */ 17 | public interface TypeSerializer { 18 | /** 19 | * Serializes a boolean schema to the target. 20 | */ 21 | void serialize(BooleanSerializableType type, T target); 22 | 23 | /** 24 | * Serializes a numeric range schema to the target. 25 | */ 26 | void serialize(DecimalSerializableType type, T target); 27 | 28 | /** 29 | * Serializes a fixed value schema to the target. 30 | */ 31 | void serialize(EnumSerializableType type, T target); 32 | 33 | /** 34 | * Serializes a list schema to the target. 35 | */ 36 | void serialize(ListSerializableType type, T target); 37 | 38 | /** 39 | * Serializes a map schema to the target. 40 | */ 41 | void serialize(MapSerializableType type, T target); 42 | 43 | /** 44 | * Serializes a record schema to the target. 45 | */ 46 | void serialize(RecordSerializableType type, T target); 47 | 48 | /** 49 | * Serializes a regex-defined string schema to the target. 50 | */ 51 | void serialize(StringSerializableType type, T target); 52 | 53 | /** 54 | * Serializes the given {@link SerializableType} to the target. 55 | * 56 | *

This method polymorphically dispatches over the other methods of this type. 57 | */ 58 | default void serializeType(SerializableType type, T target) { 59 | // named differently to ensure unrecognized subclass -> compiler error 60 | type.serialize(this, target); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/RecordConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.Map; 6 | import java.util.function.Function; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.RecordSerializableType; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 11 | 12 | /** 13 | * A {@link ConfigType} for fixed heterogeneous records. This is typically used to store POJOs or tuples, 14 | * but may be used for any parametric serialized form. 15 | * 16 | * @param The runtime type of the underlying {@link ConfigBranch} value. 17 | */ 18 | public final class RecordConfigType extends ConfigType, RecordSerializableType> { 19 | @SuppressWarnings("unchecked") 20 | public RecordConfigType(RecordSerializableType serializedType, Class runtimeType, Function, R> f, Function> f0) { 21 | super(serializedType, (Class) runtimeType, f, f0); 22 | } 23 | 24 | @Override 25 | public RecordConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 26 | return new RecordConfigType<>(this.getSerializedType(), runtimeType, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 27 | } 28 | 29 | @Override 30 | public RecordConfigType withType(RecordSerializableType newSpec) { 31 | this.checkTypeNarrowing(newSpec); 32 | return new RecordConfigType<>(newSpec, this.getRuntimeType(), this.deserializer, this.serializer); 33 | } 34 | 35 | @Override 36 | public RecordConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 37 | return processor.processRecord(this, annotation, annotated); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return this.getClass().getSimpleName() 43 | + "(" 44 | + "[" 45 | + String.join(", ", this.getSerializedType().getFields().keySet()) 46 | + "]" 47 | + " : " + this.getRuntimeType().getSimpleName() 48 | + ")"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/impl/fiber/tree/ConfigQueryTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import java.util.concurrent.atomic.AtomicReference; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.FiberQueryException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigTypes; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigQuery; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 15 | import org.junit.jupiter.api.Test; 16 | 17 | class ConfigQueryTest { 18 | @Test 19 | void run() throws FiberQueryException { 20 | AtomicReference> a = new AtomicReference<>(); 21 | ConfigTree tree = ConfigTree.builder() 22 | .fork("child") 23 | .fork("stuff") 24 | .beginValue("A", ConfigTypes.INTEGER, 10) 25 | .finishValue(a::set) 26 | .finishBranch() 27 | .finishBranch() 28 | .build(); 29 | ConfigQuery query1 = ConfigQuery.leaf(ConfigTypes.INTEGER.getSerializedType(), "child", "stuff", "A"); 30 | assertEquals(a.get(), query1.run(tree)); 31 | assertTrue(query1.search(tree).isPresent()); 32 | 33 | ConfigQuery query2 = ConfigQuery.leaf(ConfigTypes.INTEGER.getSerializedType(), "child", "more"); 34 | assertFalse(query2.search(tree).isPresent()); 35 | assertThrows(FiberQueryException.MissingChild.class, () -> query2.run(tree)); 36 | 37 | ConfigQuery query3 = ConfigQuery.branch("child", "stuff", "A"); 38 | assertThrows(FiberQueryException.WrongType.class, () -> query3.run(tree)); 39 | assertFalse(query3.search(tree).isPresent()); 40 | 41 | ConfigQuery query4 = ConfigQuery.leaf(ConfigTypes.STRING.getSerializedType(), "child", "stuff", "A"); 42 | assertThrows(FiberQueryException.WrongType.class, () -> query4.run(tree)); 43 | assertFalse(query4.search(tree).isPresent()); 44 | 45 | ConfigQuery query5 = ConfigQuery.leaf(ConfigTypes.INTEGER.getSerializedType(), "child", "stuff", "A", "more"); 46 | assertThrows(FiberQueryException.WrongType.class, () -> query5.run(tree)); 47 | assertFalse(query5.search(tree).isPresent()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/TypeCheckResult.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.util.Optional; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | /** 8 | * The result of type checking a serialized value. 9 | * 10 | * @param the actual type of the tested value 11 | * @see SerializableType#test(Object) 12 | */ 13 | public final class TypeCheckResult { 14 | /** 15 | * The singleton unrecoverable failed result. 16 | */ 17 | private static final TypeCheckResult UNRECOVERABLE = new TypeCheckResult<>(false, null); 18 | 19 | /** 20 | * Creates a successful {@link TypeCheckResult} with the given value. 21 | */ 22 | public static TypeCheckResult successful(V initialValue) { 23 | return new TypeCheckResult<>(true, initialValue); 24 | } 25 | 26 | /** 27 | * Creates a failed {@link TypeCheckResult} with the given value. 28 | */ 29 | public static TypeCheckResult failed(V correctedValue) { 30 | return new TypeCheckResult<>(false, correctedValue); 31 | } 32 | 33 | /** 34 | * Creates a failed {@link TypeCheckResult} with no value. 35 | */ 36 | public static TypeCheckResult unrecoverable() { 37 | @SuppressWarnings("unchecked") TypeCheckResult t = (TypeCheckResult) UNRECOVERABLE; 38 | return t; 39 | } 40 | 41 | private final boolean passed; 42 | @Nullable 43 | private final V correctedValue; 44 | 45 | private TypeCheckResult(boolean passed, @Nullable V correctedValue) { 46 | this.passed = passed; 47 | this.correctedValue = correctedValue; 48 | } 49 | 50 | /** 51 | * Returns {@code true} if the tested value passed the type check. 52 | * 53 | *

A value passes if it matches every attribute set on 54 | * the {@link SerializableType}. If at least one constraint check failed, 55 | * this method returns {@code false}. 56 | * 57 | * @return {@code true} if the test passed, {@code false} otherwise. 58 | */ 59 | public boolean hasPassed() { 60 | return this.passed; 61 | } 62 | 63 | /** 64 | * Returns a possible corrected value based on the tested value. 65 | * 66 | *

If the test passes, this method returns an {@code Optional} describing 67 | * the tested value. 68 | * 69 | * @return an {@code Optional} describing a possible corrected value, 70 | * or an empty {@code Optional} if the test was unrecoverable. 71 | */ 72 | public Optional getCorrectedValue() { 73 | return Optional.ofNullable(this.correctedValue); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/EnumSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collections; 5 | import java.util.LinkedHashSet; 6 | import java.util.Objects; 7 | import java.util.Set; 8 | import java.util.StringJoiner; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 13 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.EnumConstraintChecker; 14 | 15 | /** 16 | * The {@link SerializableType} for fixed sets of {@link String} values. 17 | */ 18 | public final class EnumSerializableType extends PlainSerializableType { 19 | private final Set validValues; 20 | 21 | public EnumSerializableType(String... validValues) { 22 | this(new LinkedHashSet<>(Arrays.asList(validValues))); 23 | } 24 | 25 | public EnumSerializableType(Set validValues) { 26 | super(String.class, EnumConstraintChecker.instance()); 27 | validValues.forEach(Objects::requireNonNull); 28 | this.validValues = Collections.unmodifiableSet(new LinkedHashSet<>(validValues)); 29 | } 30 | 31 | public Set getValidValues() { 32 | return this.validValues; 33 | } 34 | 35 | @Override 36 | public void serialize(TypeSerializer serializer, S target) { 37 | serializer.serialize(this, target); 38 | } 39 | 40 | @Override 41 | public S serializeValue(String value, ValueSerializer serializer) { 42 | return serializer.serializeEnum(value, this); 43 | } 44 | 45 | @Override 46 | public String deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 47 | return serializer.deserializeEnum(elem, this); 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (o == null || this.getClass() != o.getClass()) return false; 54 | EnumSerializableType that = (EnumSerializableType) o; 55 | return Objects.equals(this.validValues, that.validValues); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(this.validValues); 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return new StringJoiner(", ", EnumSerializableType.class.getSimpleName() + "[", "]") 66 | .add("validValues=" + validValues) 67 | .toString(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/NodeOperations.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1; 2 | 3 | import java.util.Iterator; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.DuplicateChildException; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.RuntimeFiberException; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.Property; 11 | 12 | /** 13 | * Static utility class for operations on {@link ConfigNode} objects. 14 | * 15 | * @see ConfigTree 16 | * @see ConfigNode 17 | */ 18 | public class NodeOperations { 19 | /** 20 | * Merges two {@code ConfigTree}s. 21 | * 22 | *

The first parameter {@code from} will be stripped of its children, 23 | * and {@code to} will receive all of {@code from}'s children. 24 | * 25 | *

If both nodes have one or more children with the same name, the child from {@code from} takes priority. 26 | * 27 | * @param from The {@code ConfigNode} that will be read from, but not mutated. 28 | * @param to The mutated {@link ConfigBranch} that will inherit from's values and nodes. 29 | */ 30 | public static void moveChildren(ConfigTree from, ConfigTree to) { 31 | try { 32 | for (Iterator it = from.getItems().iterator(); it.hasNext(); ) { 33 | ConfigNode item = it.next(); 34 | it.remove(); 35 | to.getItems().add(item, true); 36 | } 37 | } catch (DuplicateChildException e) { 38 | throw new RuntimeFiberException("Failed to merge nodes", e); 39 | } 40 | } 41 | 42 | /** 43 | * Moves a node ({@code ConfigNode}) to a new parent {@code ConfigTree}. 44 | * 45 | *

If the moved node has an existing parent, it will be detached. 46 | * If the new parent has an existing node with the same name, it will be overwritten. 47 | * 48 | * @param value The leaf node to be inherited 49 | * @param to The mutated {@link ConfigBranch} that will inherit value 50 | */ 51 | public static void moveNode(ConfigNode value, ConfigTree to) { 52 | try { 53 | value.detach(); 54 | to.getItems().add(value, true); 55 | } catch (DuplicateChildException e) { 56 | throw new RuntimeFiberException("Failed to merge value", e); 57 | } 58 | } 59 | 60 | public static void copyValue(Property from, Property to) { 61 | to.setValue(from.getValue()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/MapConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.MapSerializableType; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 9 | 10 | public class MapConstraintChecker extends ConstraintChecker, MapSerializableType> { 11 | private static final MapConstraintChecker INSTANCE = new MapConstraintChecker<>(); 12 | 13 | public static MapConstraintChecker instance() { 14 | @SuppressWarnings("unchecked") MapConstraintChecker t = (MapConstraintChecker) INSTANCE; 15 | return t; 16 | } 17 | 18 | private MapConstraintChecker() { 19 | } 20 | 21 | @Override 22 | public TypeCheckResult> test(MapSerializableType cfg, Map values) { 23 | boolean valid = true; 24 | int maxSize = cfg.getMaxSize(); 25 | Map corrected = new LinkedHashMap<>(); 26 | 27 | for (Map.Entry entry : values.entrySet()) { 28 | if (corrected.size() >= maxSize) { 29 | valid = false; 30 | break; 31 | } 32 | 33 | TypeCheckResult keyTestResult = cfg.getKeyType().test(entry.getKey()); 34 | TypeCheckResult valueTestResult = cfg.getValueType().test(entry.getValue()); 35 | 36 | if (keyTestResult.hasPassed() && valueTestResult.hasPassed()) { 37 | corrected.put(entry.getKey(), entry.getValue()); 38 | } else { 39 | valid = false; 40 | Optional correctedKey = keyTestResult.getCorrectedValue(); 41 | Optional correctedValue = valueTestResult.getCorrectedValue(); 42 | 43 | if (correctedKey.isPresent() && correctedValue.isPresent()) { 44 | corrected.put(correctedKey.get(), correctedValue.get()); 45 | } 46 | 47 | // if key or value missing, just skip the entry 48 | } 49 | } 50 | 51 | if (corrected.size() < cfg.getMinSize()) { 52 | return TypeCheckResult.unrecoverable(); 53 | } else if (!valid) { 54 | return TypeCheckResult.failed(corrected); 55 | } else { 56 | return TypeCheckResult.successful(values); 57 | } 58 | } 59 | 60 | @Override 61 | public boolean comprehends(MapSerializableType cfg, MapSerializableType cfg2) { 62 | if (cfg.getMinSize() > cfg2.getMinSize()) { 63 | return false; 64 | } 65 | 66 | if (cfg.getMaxSize() < cfg2.getMaxSize()) { 67 | return false; 68 | } 69 | 70 | return cfg.getValueType().isAssignableFrom(cfg2.getValueType()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/ListConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.LinkedHashSet; 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.ListSerializableType; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 11 | 12 | /** 13 | * A component constraints is satisfied only if all elements in the aggregate type it checks satisfy the constraint. 14 | * 15 | * @param the type of elements {@code V} holds 16 | */ 17 | public final class ListConstraintChecker extends ConstraintChecker, ListSerializableType> { 18 | private static final ListConstraintChecker INSTANCE = new ListConstraintChecker<>(); 19 | 20 | public static ListConstraintChecker instance() { 21 | @SuppressWarnings("unchecked") ListConstraintChecker t = (ListConstraintChecker) INSTANCE; 22 | return t; 23 | } 24 | 25 | private ListConstraintChecker() { 26 | } 27 | 28 | @Override 29 | public TypeCheckResult> test(ListSerializableType cfg, List values) { 30 | boolean valid = true; 31 | int maxSize = cfg.getMaxSize(); 32 | Collection corrected = cfg.hasUniqueElements() ? new LinkedHashSet<>(values.size()) : new ArrayList<>(values.size()); 33 | 34 | for (E e : values) { 35 | if (corrected.size() >= maxSize) { 36 | valid = false; 37 | break; 38 | } 39 | 40 | TypeCheckResult testResult = cfg.getElementType().test(e); 41 | 42 | if (testResult.hasPassed()) { 43 | valid &= corrected.add(e); // UNIQUE check 44 | } else { 45 | valid = false; 46 | Optional correctedValue = testResult.getCorrectedValue(); 47 | correctedValue.ifPresent(corrected::add); 48 | // if not present, just skip it 49 | } 50 | } 51 | 52 | if (corrected.size() < cfg.getMinSize()) { 53 | return TypeCheckResult.unrecoverable(); 54 | } 55 | 56 | return valid ? TypeCheckResult.successful(values) : TypeCheckResult.failed(new ArrayList<>(corrected)); 57 | } 58 | 59 | @Override 60 | public boolean comprehends(ListSerializableType cfg, ListSerializableType cfg2) { 61 | if (cfg.getMinSize() > cfg2.getMinSize()) { 62 | return false; 63 | } 64 | 65 | if (cfg.getMaxSize() < cfg2.getMaxSize()) { 66 | return false; 67 | } 68 | 69 | if (!cfg.getElementType().isAssignableFrom(cfg2.getElementType())) { 70 | return false; 71 | } 72 | 73 | // "not unique" comprehends unique 74 | return !cfg.hasUniqueElements() || cfg2.hasUniqueElements(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/SerializableTypesTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class SerializableTypesTest { 13 | @DisplayName("Test decimal type comprehension") 14 | @Test 15 | void testDecimalType() { 16 | DecimalSerializableType typeA = new DecimalSerializableType(BigDecimal.TEN.negate(), null, null); 17 | DecimalSerializableType typeB = new DecimalSerializableType(null, BigDecimal.TEN, null); 18 | assertFalse(typeA.isAssignableFrom(typeB)); 19 | assertFalse(typeB.isAssignableFrom(typeA)); 20 | DecimalSerializableType typeC = new DecimalSerializableType(BigDecimal.TEN.negate(), BigDecimal.TEN, null); 21 | assertTrue(typeA.isAssignableFrom(typeC)); 22 | assertTrue(typeB.isAssignableFrom(typeC)); 23 | assertFalse(typeC.isAssignableFrom(typeA)); 24 | assertFalse(typeC.isAssignableFrom(typeB)); 25 | assertThrows(IllegalStateException.class, () -> new DecimalSerializableType(null, null, BigDecimal.ONE)); 26 | DecimalSerializableType typeD = new DecimalSerializableType(BigDecimal.TEN.negate(), BigDecimal.TEN, BigDecimal.ONE); 27 | assertTrue(typeA.isAssignableFrom(typeD)); 28 | assertTrue(typeB.isAssignableFrom(typeD)); 29 | assertTrue(typeC.isAssignableFrom(typeD)); 30 | assertFalse(typeD.isAssignableFrom(typeA)); 31 | assertFalse(typeD.isAssignableFrom(typeB)); 32 | assertFalse(typeD.isAssignableFrom(typeC)); 33 | DecimalSerializableType typeE = new DecimalSerializableType(BigDecimal.ONE.negate(), BigDecimal.ONE, BigDecimal.valueOf(0.5)); 34 | assertTrue(typeA.isAssignableFrom(typeE)); 35 | assertTrue(typeB.isAssignableFrom(typeE)); 36 | assertTrue(typeC.isAssignableFrom(typeE)); 37 | assertFalse(typeD.isAssignableFrom(typeE)); 38 | assertFalse(typeE.isAssignableFrom(typeA)); 39 | assertFalse(typeE.isAssignableFrom(typeB)); 40 | assertFalse(typeE.isAssignableFrom(typeC)); 41 | assertFalse(typeE.isAssignableFrom(typeD)); 42 | DecimalSerializableType typeF = new DecimalSerializableType(BigDecimal.ONE.negate(), BigDecimal.ONE, BigDecimal.ONE); 43 | assertTrue(typeE.isAssignableFrom(typeF)); 44 | assertFalse(typeF.isAssignableFrom(typeE)); 45 | DecimalSerializableType typeG = new DecimalSerializableType(BigDecimal.ONE.negate(), BigDecimal.ONE, BigDecimal.ONE.setScale(100, BigDecimal.ROUND_UNNECESSARY)); 46 | assertTrue(typeF.isAssignableFrom(typeG)); 47 | assertTrue(typeG.isAssignableFrom(typeF)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/MapConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.Map; 6 | import java.util.function.Function; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.MapSerializableType; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.StringSerializableType; 11 | 12 | /** 13 | * A {@link ConfigType} for mappings between a key type and a value type. 14 | * 15 | * @param The runtime type of the underlying {@link Map} value. 16 | * @param The value type stored in the underlying {@link Map} value. 17 | */ 18 | public final class MapConfigType extends ConfigType, MapSerializableType> { 19 | @SuppressWarnings("unchecked") 20 | public MapConfigType(MapSerializableType serializedType, Class runtimeType, Function, R> f, Function> f0) { 21 | super(serializedType, (Class) runtimeType, f, f0); 22 | } 23 | 24 | @Override 25 | public MapConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 26 | return new MapConfigType<>(this.getSerializedType(), runtimeType, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 27 | } 28 | 29 | @Override 30 | public MapConfigType withType(MapSerializableType newSpec) { 31 | this.checkTypeNarrowing(newSpec); 32 | return new MapConfigType<>(newSpec, this.getRuntimeType(), this.deserializer, this.serializer); 33 | } 34 | 35 | /** 36 | * Creates a new {@link MapConfigType} with a minimum size constraint. 37 | */ 38 | public MapConfigType withMinSize(int min) { 39 | MapSerializableType current = this.getSerializedType(); 40 | return this.withType(new MapSerializableType<>(StringSerializableType.DEFAULT_STRING, current.getValueType(), min, current.getMaxSize())); 41 | } 42 | 43 | /** 44 | * Creates a new {@link MapConfigType} with a maximum size constraint. 45 | */ 46 | public MapConfigType withMaxSize(int max) { 47 | MapSerializableType current = this.getSerializedType(); 48 | return this.withType(new MapSerializableType<>(StringSerializableType.DEFAULT_STRING, current.getValueType(), current.getMinSize(), max)); 49 | } 50 | 51 | @Override 52 | public MapConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 53 | return processor.processMap(this, annotation, annotated); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/annotation/collect/MemberCollectorImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.annotation.collect; 2 | 3 | import java.lang.reflect.AccessibleObject; 4 | import java.lang.reflect.Field; 5 | import java.lang.reflect.Member; 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Modifier; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.Listener; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.Setting; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.Settings; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.collect.MemberCollector; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.collect.PojoMemberProcessor; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ProcessingMemberException; 15 | 16 | public class MemberCollectorImpl implements MemberCollector { 17 | private final boolean onlyAnnotated; 18 | 19 | public MemberCollectorImpl(boolean onlyAnnotated) { 20 | this.onlyAnnotated = onlyAnnotated; 21 | } 22 | 23 | private boolean isIncluded(Member member) { 24 | if (member.isSynthetic() || Modifier.isTransient(member.getModifiers())) return false; 25 | 26 | // Assume defaults, see Settings annotation 27 | boolean onlyAnnotated = this.onlyAnnotated; 28 | 29 | Class owningClass = member.getDeclaringClass(); 30 | 31 | if (owningClass.isAnnotationPresent(Settings.class)) { 32 | onlyAnnotated = owningClass.getAnnotation(Settings.class).onlyAnnotated(); 33 | } 34 | 35 | if (member instanceof AccessibleObject) { 36 | AccessibleObject object = (AccessibleObject) member; 37 | 38 | if (object.isAnnotationPresent(Setting.class)) { 39 | return !object.getAnnotation(Setting.class).ignore(); 40 | } 41 | 42 | return object.isAnnotationPresent(Listener.class) || !onlyAnnotated; 43 | } 44 | 45 | return false; 46 | } 47 | 48 | @Override 49 | public

void collect(P pojo, Class clazz, PojoMemberProcessor processor) throws ProcessingMemberException { 50 | for (Method m : clazz.getDeclaredMethods()) { 51 | if (isIncluded(m) && m.isAnnotationPresent(Listener.class)) { 52 | processor.processListenerMethod(pojo, m, m.getAnnotation(Listener.class).value()); 53 | } 54 | } 55 | 56 | for (Field f : clazz.getDeclaredFields()) { 57 | if (isIncluded(f) && f.isAnnotationPresent(Listener.class)) { 58 | processor.processListenerField(pojo, f, f.getAnnotation(Listener.class).value()); 59 | } 60 | } 61 | 62 | for (Field f : clazz.getDeclaredFields()) { 63 | if (isIncluded(f) && !f.isAnnotationPresent(Listener.class)) { 64 | if (f.isAnnotationPresent(Setting.Group.class)) { 65 | processor.processGroup(pojo, f); 66 | } else { 67 | processor.processSetting(pojo, f); 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/RecordConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.RecordSerializableType; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 10 | 11 | public class RecordConstraintChecker extends ConstraintChecker, RecordSerializableType> { 12 | private static final RecordConstraintChecker INSTANCE = new RecordConstraintChecker(); 13 | 14 | public static RecordConstraintChecker instance() { 15 | return INSTANCE; 16 | } 17 | 18 | private RecordConstraintChecker() { 19 | } 20 | 21 | @Override 22 | public TypeCheckResult> test(RecordSerializableType cfg, Map value) { 23 | // if value does not have enough fields -> unrecoverable 24 | if (!value.keySet().containsAll(cfg.getFields().keySet())) { 25 | return TypeCheckResult.unrecoverable(); 26 | } 27 | 28 | // if value has extra fields -> failed 29 | boolean successful = cfg.getFields().keySet().containsAll(value.keySet()); 30 | // keep track of a corrected value map 31 | Map corrected = new LinkedHashMap<>(value.size()); 32 | 33 | for (Map.Entry> field : cfg.getFields().entrySet()) { 34 | Object child = value.get(field.getKey()); 35 | SerializableType fieldType = field.getValue(); 36 | TypeCheckResult result = this.testChild(fieldType, child); 37 | Optional correctedFieldValue = result.getCorrectedValue(); 38 | 39 | if (!result.hasPassed()) { 40 | successful = false; 41 | } 42 | 43 | if (correctedFieldValue.isPresent()) { 44 | corrected.put(field.getKey(), correctedFieldValue.get()); 45 | } else { 46 | return TypeCheckResult.unrecoverable(); 47 | } 48 | } 49 | 50 | return successful ? TypeCheckResult.successful(value) : TypeCheckResult.failed(corrected); 51 | } 52 | 53 | @SuppressWarnings("unchecked") 54 | private TypeCheckResult testChild(SerializableType type, Object value) { 55 | // value has already been validated, so this is always valid 56 | // type.test also calls type.cast inside it, so double casting serves no purpose 57 | return type.test((T) value); 58 | } 59 | 60 | @Override 61 | public boolean comprehends(RecordSerializableType cfg, RecordSerializableType cfg2) { 62 | for (Map.Entry> entry : cfg.getFields().entrySet()) { 63 | SerializableType other = cfg2.getFields().get(entry.getKey()); 64 | 65 | if (!entry.getValue().equals(other)) { 66 | return false; 67 | } 68 | } 69 | 70 | return true; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/constraint/DecimalConstraintChecker.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.constraint; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.DecimalSerializableType; 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 7 | 8 | /** 9 | * Checks validity of serialized numbers based on a {@code DecimalConfigType}'s range constraint. 10 | */ 11 | public final class DecimalConstraintChecker extends ConstraintChecker { 12 | private static final DecimalConstraintChecker INSTANCE = new DecimalConstraintChecker(); 13 | 14 | public static DecimalConstraintChecker instance() { 15 | return INSTANCE; 16 | } 17 | 18 | private DecimalConstraintChecker() { 19 | } 20 | 21 | @Override 22 | public TypeCheckResult test(DecimalSerializableType cfg, BigDecimal value) { 23 | if (cfg.getMinimum() != null && cfg.getMinimum().compareTo(value) > 0) { 24 | return TypeCheckResult.failed(cfg.getMinimum()); 25 | } else if (cfg.getMaximum() != null && cfg.getMaximum().compareTo(value) < 0) { 26 | return TypeCheckResult.failed(cfg.getMaximum()); 27 | } else if (cfg.getIncrement() != null && value.remainder(cfg.getIncrement()).intValue() != 0) { 28 | if (cfg.getMinimum() != null) { 29 | return TypeCheckResult.failed(fit(value, cfg.getMinimum(), cfg.getIncrement())); 30 | } else { 31 | return TypeCheckResult.unrecoverable(); 32 | } 33 | } else { 34 | return TypeCheckResult.successful(value); 35 | } 36 | } 37 | 38 | @Override 39 | public boolean comprehends(DecimalSerializableType cfg, DecimalSerializableType cfg2) { 40 | if (cfg.getMinimum() == null || cfg2.getMinimum() != null && cfg.getMinimum().compareTo(cfg2.getMinimum()) <= 0) { 41 | if (cfg.getMaximum() == null || cfg2.getMaximum() != null && cfg.getMaximum().compareTo(cfg2.getMaximum()) >= 0) { 42 | return cfg.getIncrement() == null || cfg2.getIncrement() != null && cfg2.getIncrement().remainder(cfg.getIncrement()).compareTo(BigDecimal.ZERO) == 0; 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | 49 | private static BigDecimal nearest(BigDecimal less, BigDecimal value, BigDecimal more) { 50 | BigDecimal lessDiff = value.subtract(less); 51 | BigDecimal moreDiff = more.subtract(value); 52 | if (lessDiff.compareTo(moreDiff) < 0) return less; 53 | return more; 54 | } 55 | 56 | private static BigDecimal fit(BigDecimal value, BigDecimal min, BigDecimal step) { 57 | BigDecimal prevTick = ((value.subtract(min)).divide(step, BigDecimal.ROUND_FLOOR)).setScale(0, BigDecimal.ROUND_DOWN); 58 | BigDecimal prevTickValue = prevTick.multiply(step).add(min); 59 | BigDecimal nextTickValue = prevTick.add(BigDecimal.ONE).multiply(step).add(min); 60 | return nearest(prevTickValue, value, nextTickValue); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://i.imgur.com/apskb0q.png) 2 | 3 | A configuration system made for [Fabric](https://github.com/FabricMC) 4 | 5 | ## Examples 6 | 7 | In both examples we define a custom `ConfigType` for `FiberId` (a generic `Identifier`) like this: 8 | ```java 9 | StringConfigType FIBER_ID = ConfigTypes.STRING.derive( 10 | FiberId.class, 11 | s -> new FiberId(s.substring(0, s.indexOf(':')), s.substring(s.indexOf(':') + 1)), 12 | FiberId::toString 13 | ); 14 | ``` 15 | 16 | ### Builders 17 | 18 | Creating the immediate representation of your configuration using builders: 19 | ```java 20 | // Constructing the immediate representation 21 | PropertyMirror someIdentifier = PropertyMirror.create(FIBER_ID); 22 | ConfigTree tree = ConfigTree.builder() 23 | .beginValue("some_identifier", FIBER_ID, new FiberId("some", "identifier")) 24 | .withComment("This is a comment attached to some_identifier!") 25 | .finishValue(someIdentifier::mirror) 26 | .fork("gui") 27 | .withValue("opacity", ConfigTypes.FLOAT.withValidRange(0, 1, 0.1), 1f) 28 | .finishBranch() 29 | .build(); 30 | 31 | // Interacting with the configuration 32 | System.out.println(someIdentifier.getValue()); 33 | ``` 34 | 35 | ### Annotations 36 | 37 | Create a class representing the configuration: 38 | ```java 39 | @Settings(namingConvention = SnakeCaseConvention.class) 40 | private static class MyPojo { 41 | FiberId someIdentifier = new FiberId("some", "identifier"); 42 | 43 | @Setting.Group 44 | GuiGroup gui = new GuiGroup(); 45 | 46 | private static class GuiGroup { 47 | public @Setting.Constrain.Range(min = 0, max = 1, step = 0.1) float opacity = 1f; 48 | } 49 | } 50 | ``` 51 | 52 | And to turn it into an immediate representation: 53 | ```java 54 | // Registering the custom type to the annotation processor 55 | AnnotatedSettings settings = AnnotatedSettings.create(); 56 | settings.registerTypeMapping(FiberId.class, FIBER_ID); 57 | 58 | // Creating the immediate representation from the annotated POJO 59 | MyPojo pojo = new MyPojo(); 60 | ConfigTree tree = ConfigTree.builder().applyFromPojo(pojo, settings).build(); 61 | System.out.println(pojo.someIdentifier); 62 | ``` 63 | 64 | ## Getting it 65 | 66 | Add the following to your dependencies (`build.gradle`): 67 | ```gradle 68 | dependencies { 69 | ... 70 | implementation "me.zeroeightsix:fiber:${project.fiber_version}" 71 | } 72 | ``` 73 | 74 | Add `fiber_version` to your `gradle.properties`: 75 | ```properties 76 | fiber_version = 77 | ``` 78 | Replace `` with the latest version of fiber, which you can find [here](http://maven.modmuss50.me/me/zeroeightsix/fiber/). 79 | 80 | If you're using fiber in a project without fabric, you may have to add the repository as well: 81 | ```gradle 82 | repositories { 83 | maven { 84 | url = "https://maven.modmuss50.me/" 85 | } 86 | } 87 | ``` -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/api/fiber/v1/NodeOperationsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.math.BigDecimal; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigLeafBuilder; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigTreeBuilder; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigTypes; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 16 | import org.junit.jupiter.api.DisplayName; 17 | import org.junit.jupiter.api.Test; 18 | 19 | public class NodeOperationsTest { 20 | @Test 21 | @DisplayName("Node -> Node") 22 | void moveChildren() { 23 | ConfigTree treeOne = ConfigTree.builder() 24 | .withValue("A", ConfigTypes.INTEGER, 10) 25 | .build(); 26 | 27 | ConfigTreeBuilder nodeTwo = ConfigTree.builder(); 28 | 29 | NodeOperations.moveChildren(treeOne, nodeTwo); 30 | 31 | testNodeFor(nodeTwo, "A", ConfigTypes.INTEGER.getSerializedType(), BigDecimal.TEN); 32 | } 33 | 34 | @Test 35 | @DisplayName("Value -> Node") 36 | void moveNode() { 37 | ConfigTreeBuilder node = ConfigTree.builder(); 38 | ConfigLeaf value = node.beginValue("A", ConfigTypes.INTEGER.getSerializedType(), BigDecimal.TEN).build(); 39 | 40 | NodeOperations.moveNode(value, node); 41 | 42 | testNodeFor(node, "A", ConfigTypes.INTEGER.getSerializedType(), BigDecimal.TEN); 43 | } 44 | 45 | @Test 46 | @DisplayName("Value -> Value") 47 | void copyValue() { 48 | ConfigLeaf valueOne = ConfigLeafBuilder 49 | .create(null, "A", ConfigTypes.INTEGER.getSerializedType(), BigDecimal.TEN) 50 | .build(); 51 | ConfigLeaf valueTwo = ConfigLeafBuilder 52 | .create(null, "A", ConfigTypes.INTEGER.getSerializedType(), BigDecimal.valueOf(20)) 53 | .build(); 54 | 55 | NodeOperations.copyValue(valueOne, valueTwo); 56 | testItemFor(ConfigTypes.INTEGER.getSerializedType(), BigDecimal.TEN, valueTwo); 57 | } 58 | 59 | public static void testNodeFor(ConfigTree node, String name, SerializableType type, T value) { 60 | ConfigNode item = node.lookup(name); 61 | testItemFor(type, value, item); 62 | } 63 | 64 | static void testItemFor(SerializableType type, T value, ConfigNode item) { 65 | assertNotNull(item, "Setting exists"); 66 | assertTrue(item instanceof ConfigLeaf, "Setting is a property"); 67 | ConfigLeaf property = (ConfigLeaf) item; 68 | assertEquals(type, property.getConfigType(), "Setting type is correct"); 69 | assertEquals(value, property.getValue(), "Setting value is correct"); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/ListConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.List; 6 | import java.util.function.Function; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.ListSerializableType; 10 | 11 | /** 12 | * A {@link ConfigType} for collections of values. This may be used to represent lists, sets, or vectors. 13 | * 14 | * @param The runtime type of the underlying {@link List} value. 15 | * @param The element type of the underlying {@link List} value. 16 | */ 17 | public final class ListConfigType extends ConfigType, ListSerializableType> { 18 | @SuppressWarnings("unchecked") 19 | public ListConfigType(ListSerializableType serializedType, Class runtimeType, Function, R> f, Function> f0) { 20 | super(serializedType, (Class) runtimeType, f, f0); 21 | } 22 | 23 | @Override 24 | public ListConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 25 | return new ListConfigType<>(this.getSerializedType(), runtimeType, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 26 | } 27 | 28 | @Override 29 | public ListConfigType withType(ListSerializableType newSpec) { 30 | this.checkTypeNarrowing(newSpec); 31 | return new ListConfigType<>(newSpec, this.getRuntimeType(), this.deserializer, this.serializer); 32 | } 33 | 34 | @Override 35 | public ListConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 36 | return processor.processList(this, annotation, annotated); 37 | } 38 | 39 | /** 40 | * Returns a new {@link ListConfigType} with a minimum size constraint. 41 | */ 42 | public ListConfigType withMinSize(int min) { 43 | ListSerializableType current = this.getSerializedType(); 44 | return this.withType(new ListSerializableType<>(current.getElementType(), min, current.getMaxSize(), current.hasUniqueElements())); 45 | } 46 | 47 | /** 48 | * Returns a new {@link ListConfigType} with a maximum size constraint. 49 | */ 50 | public ListConfigType withMaxSize(int max) { 51 | ListSerializableType current = this.getSerializedType(); 52 | return this.withType(new ListSerializableType<>(current.getElementType(), current.getMinSize(), max, current.hasUniqueElements())); 53 | } 54 | 55 | /** 56 | * Returns a new {@link ListConfigType} with a uniqueness constraint. 57 | */ 58 | public ListConfigType withUniqueElements() { 59 | ListSerializableType current = this.getSerializedType(); 60 | return this.withType(new ListSerializableType<>(current.getElementType(), current.getMinSize(), current.getMaxSize(), true)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/ConfigLeaf.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import java.util.function.BiConsumer; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigLeafBuilder; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 10 | 11 | /** 12 | * A {@code ConfigNode} with some value of type {@code T}. 13 | * 14 | * @param The type of value this class holds 15 | * @see ConfigNode 16 | * @see ConfigLeafBuilder 17 | */ 18 | public interface ConfigLeaf extends ConfigNode, Property, Commentable { 19 | /** 20 | * Sets the value held by this {@code ConfigLeaf}. 21 | * 22 | *

If the provided value does not satisfy this setting's 23 | * {@linkplain SerializableType#test(Object) type constraints}: 24 | *

    25 | *
  • if a corrected value can be found, this setting is set to the corrected value 26 | * and this method returns {@code true}.
  • 27 | *
  • otherwise, the current value is not updated and the method returns {@code false}.
  • 28 | *
29 | * 30 | * @param value the new value this {@code ConfigLeaf} should hold 31 | * @return {@code true} if this property changed as a result of the call, and {@code false} otherwise. 32 | * @see ConfigLeaf#accepts(Object) 33 | */ 34 | @Override 35 | boolean setValue(@Nonnull T value); 36 | 37 | /** 38 | * Returns {@code true} if this property can be set to the given raw value. 39 | * 40 | *

This method does not account for possible corrections offered by the type's constraints. 41 | * In other words, it returns {@code true} if and only if every constraint of this property's 42 | * {@linkplain #getConfigType() config type} accepts the given value as is. 43 | * 44 | * @param rawValue the value to check 45 | * @return {@code true} if this property accepts the given value, {@code false} otherwise. 46 | * @see SerializableType#accepts(Object) 47 | */ 48 | default boolean accepts(@Nonnull T rawValue) { 49 | return true; 50 | } 51 | 52 | /** 53 | * Returns this {@code ConfigLeaf}'s current value. 54 | * 55 | *

If no successful call to {@link #setValue(Object)} has been made, 56 | * this method returns this node's {@linkplain #getDefaultValue() default value}. 57 | * 58 | * @return this node's value 59 | */ 60 | @Nonnull 61 | @Override 62 | T getValue(); 63 | 64 | SerializableType getConfigType(); 65 | 66 | @Override 67 | default Class getType() { 68 | return this.getConfigType().getErasedPlatformType(); 69 | } 70 | 71 | /** 72 | * Returns the listener for this item. 73 | * 74 | *

When this item's value changes, the consumer will be called with the old value as first argument and the new value as second argument. 75 | * 76 | * @return the listener 77 | */ 78 | @Nonnull 79 | BiConsumer getListener(); 80 | 81 | void addChangeListener(BiConsumer listener); 82 | 83 | /** 84 | * Returns the default value for this item. 85 | * 86 | * @return the default value 87 | */ 88 | @Nullable 89 | T getDefaultValue(); 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/NumberConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.math.BigDecimal; 6 | import java.util.function.Function; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.DecimalSerializableType; 10 | 11 | /** 12 | * A {@link ConfigType} for numeric ranges. 13 | * 14 | * @param The runtime type of the underlying {@link BigDecimal} value. 15 | */ 16 | public final class NumberConfigType extends ConfigType { 17 | public NumberConfigType(DecimalSerializableType serializedType, Class runtimeType, Function f, Function f0) { 18 | super(serializedType, runtimeType, f, f0); 19 | } 20 | 21 | @Override 22 | public NumberConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 23 | @SuppressWarnings("unchecked") Class c = (Class) runtimeType; 24 | return new NumberConfigType<>(this.getSerializedType(), c, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 25 | } 26 | 27 | @Override 28 | public NumberConfigType withType(DecimalSerializableType newSpec) { 29 | this.checkTypeNarrowing(newSpec); 30 | return new NumberConfigType<>(newSpec, this.getRuntimeType(), this.deserializer, this.serializer); 31 | } 32 | 33 | @Override 34 | public NumberConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 35 | return processor.processDecimal(this, annotation, annotated); 36 | } 37 | 38 | /** 39 | * Returns a {@link NumberConfigType} with the given minimum value. 40 | */ 41 | public NumberConfigType withMinimum(T min) { 42 | DecimalSerializableType current = this.getSerializedType(); 43 | return this.withType(new DecimalSerializableType(this.toSerializedType(min), current.getMaximum(), current.getIncrement())); 44 | } 45 | 46 | /** 47 | * Returns a {@link NumberConfigType} with the given maximum value. 48 | */ 49 | public NumberConfigType withMaximum(T max) { 50 | DecimalSerializableType current = this.getSerializedType(); 51 | return this.withType(new DecimalSerializableType(current.getMinimum(), this.toSerializedType(max), current.getIncrement())); 52 | } 53 | 54 | /** 55 | * Returns a {@link NumberConfigType} with the given step. 56 | */ 57 | public NumberConfigType withIncrement(T step) { 58 | DecimalSerializableType current = this.getSerializedType(); 59 | return this.withType(new DecimalSerializableType(current.getMinimum(), current.getMaximum(), this.toSerializedType(step))); 60 | } 61 | 62 | /** 63 | * Returns a {@link NumberConfigType} with the given range. 64 | */ 65 | public NumberConfigType withValidRange(T min, T max, T step) { 66 | return this.withType(new DecimalSerializableType(this.toSerializedType(min), this.toSerializedType(max), this.toSerializedType(step))); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/StringConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.function.Function; 6 | import java.util.regex.Pattern; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.StringSerializableType; 10 | 11 | /** 12 | * A {@link ConfigType} for regex-defined string values. 13 | * 14 | * @param The runtime type of the underlying {@link String} value. 15 | */ 16 | public final class StringConfigType extends ConfigType { 17 | public StringConfigType(StringSerializableType serializedType, Class runtimeType, Function f, Function f0) { 18 | super(serializedType, runtimeType, f, f0); 19 | } 20 | 21 | @Override 22 | public StringConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 23 | @SuppressWarnings("unchecked") Class c = (Class) runtimeType; 24 | return new StringConfigType<>(this.getSerializedType(), c, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 25 | } 26 | 27 | @Override 28 | public StringConfigType withType(StringSerializableType newSpec) { 29 | this.checkTypeNarrowing(newSpec); 30 | return new StringConfigType<>(newSpec, this.getRuntimeType(), this.deserializer, this.serializer); 31 | } 32 | 33 | @Override 34 | public StringConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 35 | return processor.processString(this, annotation, annotated); 36 | } 37 | 38 | /** 39 | * Returns a new {@link StringConfigType} with a minimum length constraint. 40 | */ 41 | public StringConfigType withMinLength(int min) { 42 | StringSerializableType current = this.getSerializedType(); 43 | return this.withType(new StringSerializableType(min, current.getMaxLength(), current.getPattern())); 44 | } 45 | 46 | /** 47 | * Returns a new {@link StringConfigType} with a maximum length constraint. 48 | */ 49 | public StringConfigType withMaxLength(int max) { 50 | StringSerializableType current = this.getSerializedType(); 51 | return this.withType(new StringSerializableType(current.getMinLength(), max, current.getPattern())); 52 | } 53 | 54 | /** 55 | * Returns a new {@link StringConfigType} with a regex constraint defined by the given pattern string. 56 | * 57 | * @throws java.util.regex.PatternSyntaxException If regex is not a valid {@link Pattern}. 58 | * @see #withPattern(Pattern) 59 | */ 60 | public StringConfigType withPattern(String regex) { 61 | return this.withPattern(Pattern.compile(regex)); 62 | } 63 | 64 | /** 65 | * Returns a new {@link StringConfigType} with a regex constraint defined by the given pattern. 66 | */ 67 | public StringConfigType withPattern(Pattern pattern) { 68 | StringSerializableType current = this.getSerializedType(); 69 | return this.withType(new StringSerializableType(current.getMinLength(), current.getMaxLength(), pattern)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/ConfigTree.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.NodeOperations; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigTreeBuilder; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 9 | 10 | /** 11 | * A container for a tree structure of {@link ConfigNode}. 12 | */ 13 | public interface ConfigTree { 14 | /** 15 | * Creates a new builder for a root config node. 16 | */ 17 | static ConfigTreeBuilder builder() { 18 | return builder(null, null); 19 | } 20 | 21 | /** 22 | * Creates a new non-root {@link ConfigTreeBuilder} with a name. 23 | * 24 | *

The built subtree will be attached to {@code parent}, with the given {@code name}. 25 | * To generate a whole tree from the root, use {@link ConfigTree#builder()} instead. 26 | * 27 | * @param parent the parent of the builder to create 28 | * @param name the name of the {@link ConfigTree} created by the builder 29 | * @return a {@code ConfigTreeBuilder} for an intermediary config branch 30 | * @see ConfigTree#builder() 31 | */ 32 | static ConfigTreeBuilder builder(@Nullable ConfigTree parent, @Nullable String name) { 33 | return new ConfigTreeBuilder(parent, name); 34 | } 35 | 36 | /** 37 | * Returns a collection of this node's children. 38 | * 39 | *

The returned collection is guaranteed to have no two nodes with the same name. 40 | * Any changes made to it will be reflected by this tree. 41 | * 42 | * @return the set of children 43 | * @see NodeOperations 44 | */ 45 | @Nonnull 46 | NodeCollection getItems(); 47 | 48 | /** 49 | * Tries to find a child in this node by name. If a child is found, it will be returned. 50 | * 51 | * @param name The name of the child to look for. 52 | * @return the child if found, otherwise {@code null}. 53 | */ 54 | @Nullable 55 | ConfigNode lookup(String name); 56 | 57 | /** 58 | * Tries to find a child branch in this node by name. If a child is found, and it is 59 | * a branch node, it is returned. 60 | * 61 | * @param name The name of the child to look for. 62 | * @return The child branch if found, otherwise null. 63 | */ 64 | @Nullable 65 | ConfigBranch lookupBranch(String name); 66 | 67 | /** 68 | * Tries to find a child leaf in this node by name. 69 | * If a child with the right type is found, it will be returned. 70 | * 71 | * @param name the name of the leaf to look for 72 | * @param type a {@link SerializableType} object representing the type of values held by the leaf 73 | * @param the type of values held by the leaf 74 | * @return the leaf if found, otherwise {@code null} 75 | */ 76 | @Nullable 77 | // should we throw an exception on wrong type instead ? 78 | ConfigLeaf lookupLeaf(String name, SerializableType type); 79 | 80 | /** 81 | * Tries to find a child leaf in this node by name. 82 | * If a child with the right type is found, the mirror will be bound to it. 83 | * 84 | * @param name the name of the leaf to mirror 85 | * @param mirror the mirror to bind to the leaf 86 | * @return {@code true} if the operation succeeded 87 | */ 88 | boolean lookupAndBind(String name, PropertyMirror mirror); 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/tree/PropertyMirrorImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import java.util.Objects; 4 | 5 | import javax.annotation.Nonnull; 6 | import javax.annotation.Nullable; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.Property; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.PropertyMirror; 12 | 13 | public final class PropertyMirrorImpl implements PropertyMirror { 14 | protected Property delegate; 15 | protected ConfigType mirroredType; 16 | @Nullable 17 | private S lastSerializedValue; 18 | @Nullable 19 | private R cachedValue; 20 | 21 | public PropertyMirrorImpl(ConfigType mirroredType) { 22 | this.mirroredType = mirroredType; 23 | } 24 | 25 | /** 26 | * Sets a property to mirror. 27 | * 28 | *

After calling this method with a valid delegate, 29 | * every property method will redirect to {@code delegate}. 30 | * 31 | * @param delegate a property to mirror 32 | */ 33 | @Override 34 | public void mirror(Property delegate) { 35 | if (!this.mirroredType.getSerializedType().getErasedPlatformType().equals(delegate.getType())) { 36 | throw new IllegalArgumentException("Unsupported delegate type " + delegate.getType() + ", should be " + this.mirroredType.getSerializedType().getErasedPlatformType()); 37 | } 38 | 39 | @SuppressWarnings("unchecked") Property d = (Property) delegate; 40 | this.delegate = d; 41 | 42 | if (d instanceof ConfigLeaf) { 43 | // passive invalidation 44 | ((ConfigLeaf) d).addChangeListener((old, cur) -> this.cachedValue = null); 45 | this.lastSerializedValue = null; 46 | } else { 47 | // active invalidation, less efficient 48 | this.lastSerializedValue = d.getValue(); 49 | } 50 | } 51 | 52 | @Override 53 | public Property getMirrored() { 54 | return this.delegate; 55 | } 56 | 57 | @Override 58 | public boolean setValue(@Nonnull R value) { 59 | if (this.delegate == null) throw new IllegalStateException("No delegate property set for this mirror"); 60 | return this.delegate.setValue(this.mirroredType.toPlatformType(value)); 61 | } 62 | 63 | @Override 64 | public boolean accepts(@Nonnull R value) { 65 | if (this.delegate == null) throw new IllegalStateException("No delegate property set for this mirror"); 66 | return this.delegate.accepts(this.mirroredType.toPlatformType(value)); 67 | } 68 | 69 | @Nonnull 70 | @Override 71 | public R getValue() { 72 | if (this.delegate == null) throw new IllegalStateException("No delegate property set for this mirror"); 73 | 74 | if (this.cachedValue == null || this.lastSerializedValue != null) { 75 | S serializedValue = this.delegate.getValue(); 76 | 77 | if (cachedValue == null || !Objects.equals(this.lastSerializedValue, serializedValue)) { 78 | this.cachedValue = this.mirroredType.toRuntimeType(serializedValue); 79 | this.lastSerializedValue = serializedValue; 80 | } 81 | } 82 | 83 | return this.cachedValue; 84 | } 85 | 86 | @Override 87 | public Class getType() { 88 | return this.mirroredType.getRuntimeType(); 89 | } 90 | 91 | @Override 92 | public ConfigType getMirroredType() { 93 | return this.mirroredType; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/tree/ConfigLeafImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import java.util.Objects; 4 | import java.util.function.BiConsumer; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigLeafBuilder; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.TypeCheckResult; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 13 | 14 | public final class ConfigLeafImpl extends ConfigNodeImpl implements ConfigLeaf { 15 | private T value; 16 | @Nonnull 17 | private final T defaultValue; 18 | @Nonnull 19 | private BiConsumer listener; 20 | @Nonnull 21 | private final SerializableType type; 22 | 23 | /** 24 | * Creates a {@code ConfigLeaf}. 25 | * 26 | * @param name the name for this node 27 | * @param type the type of value this item holds 28 | * @param comment the comment for this node 29 | * @param defaultValue the default value for this node 30 | * @param listener the consumer or listener for this item. When this item's value changes, the consumer will be called with the old value as first argument and the new value as second argument. 31 | * @see ConfigLeafBuilder 32 | */ 33 | public ConfigLeafImpl(@Nonnull String name, @Nonnull SerializableType type, @Nullable String comment, @Nonnull T defaultValue, @Nonnull BiConsumer listener) { 34 | super(name, comment); 35 | this.defaultValue = Objects.requireNonNull(defaultValue); 36 | this.listener = listener; 37 | this.type = type; 38 | this.setValue(defaultValue); 39 | } 40 | 41 | @Override 42 | @Nonnull 43 | public T getValue() { 44 | return this.value; 45 | } 46 | 47 | @Override 48 | public SerializableType getConfigType() { 49 | return this.type; 50 | } 51 | 52 | @Override 53 | public boolean accepts(@Nonnull T value) { 54 | return this.type.accepts(value); 55 | } 56 | 57 | @Override 58 | public boolean setValue(@Nonnull T value) { 59 | T correctedValue; 60 | TypeCheckResult result = this.type.test(value); 61 | 62 | if (result.hasPassed()) { 63 | correctedValue = value; 64 | } else { 65 | if (!result.getCorrectedValue().isPresent()) { 66 | return false; 67 | } 68 | 69 | correctedValue = result.getCorrectedValue().get(); 70 | } 71 | 72 | T oldValue = this.value; 73 | this.value = Objects.requireNonNull(correctedValue); 74 | this.listener.accept(oldValue, this.value); 75 | return true; 76 | } 77 | 78 | @Override 79 | @Nonnull 80 | public BiConsumer getListener() { 81 | return listener; 82 | } 83 | 84 | @Override 85 | public void addChangeListener(BiConsumer listener) { 86 | this.listener = this.listener.andThen(listener); 87 | } 88 | 89 | @Override 90 | @Nonnull 91 | public T getDefaultValue() { 92 | return defaultValue; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return this.getClass().getSimpleName() 98 | + '<' + this.type.getGenericPlatformType().getTypeName() 99 | + ">[name=" + this.getName() 100 | + ", comment=" + this.getComment() 101 | + ", value=" + this.getValue() 102 | + "]"; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/EnumConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.Arrays; 6 | import java.util.Set; 7 | import java.util.function.Function; 8 | import java.util.stream.Collectors; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.EnumSerializableType; 12 | 13 | /** 14 | * A {@link ConfigType} for a finite set of values. These are often the constants of a Java enum, 15 | * but are represented as strings here in order to support serialization and deserialization. 16 | * 17 | * @param The runtime type of the underlying finite string value. 18 | */ 19 | public final class EnumConfigType extends ConfigType { 20 | public EnumConfigType(EnumSerializableType serializedType, Class runtimeType, Function f, Function f0) { 21 | super(serializedType, runtimeType, f, f0); 22 | } 23 | 24 | @Override 25 | public EnumConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer) { 26 | @SuppressWarnings("unchecked") Class c = (Class) runtimeType; 27 | return new EnumConfigType<>(this.getSerializedType(), c, s -> partialDeserializer.apply(this.deserializer.apply(s)), u -> this.serializer.apply(partialSerializer.apply(u))); 28 | } 29 | 30 | @Override 31 | public EnumConfigType withType(EnumSerializableType newSpec) { 32 | this.checkTypeNarrowing(newSpec); 33 | return new EnumConfigType<>(newSpec, this.getRuntimeType(), this.deserializer, this.serializer); 34 | } 35 | 36 | @Override 37 | public EnumConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated) { 38 | return processor.processEnum(this, annotation, annotated); 39 | } 40 | 41 | /** 42 | * Returns a new {@link EnumConfigType} that represents the given set of values. The values 43 | * are converted to the serialized type {@link String} before being stored as the new type's constraints. 44 | * 45 | * @param values A Set of values of the runtime type whose serialized forms are the acceptable values. 46 | * @return A new EnumConfigType with the provided constraint. 47 | * @see #withValues(Object[]) 48 | */ 49 | public EnumConfigType withValues(Set values) { 50 | Set strValues = values.stream().map(this::toSerializedType).collect(Collectors.toSet()); 51 | return this.withType(new EnumSerializableType(strValues)); 52 | } 53 | 54 | /** 55 | * Returns a new {@link EnumConfigType} that represents the given set of values. The values 56 | * are converted to the serialized type {@link String} before being stored as the new type's constraints. 57 | * 58 | * @param values A Set of values of the runtime type whose serialized forms are the acceptable values. 59 | * @return A new EnumConfigType with the provided constraint. 60 | * @see #withValues(Set) 61 | */ 62 | @SafeVarargs 63 | public final EnumConfigType withValues(T... values) { 64 | Set strValues = Arrays.stream(values).map(this::toSerializedType).collect(Collectors.toSet()); 65 | return this.withType(new EnumSerializableType(strValues)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/RecordSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.StringJoiner; 7 | import java.util.stream.Collectors; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 14 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.RecordConstraintChecker; 15 | 16 | /** 17 | * The {@link SerializableType} for fixed heterogeneous records. 18 | */ 19 | public final class RecordSerializableType extends ParameterizedSerializableType> { 20 | private final Map> fields; 21 | 22 | public RecordSerializableType(Map> fields) { 23 | super(Map.class, RecordConstraintChecker.instance()); 24 | fields.keySet().forEach(Objects::requireNonNull); 25 | this.fields = fields; 26 | } 27 | 28 | public Map> getFields() { 29 | return this.fields; 30 | } 31 | 32 | @Override 33 | public ParameterizedType getParameterizedType() { 34 | return new ParameterizedTypeImpl(this.getErasedPlatformType(), String.class, Object.class); 35 | } 36 | 37 | @SuppressWarnings("unchecked") 38 | @Override 39 | public Map cast(@Nonnull Object value) { 40 | Map map = (Map) value; 41 | 42 | // we can potentially allow extra fields in value, but choose not to allow them for now 43 | if (!this.fields.keySet().equals(map.keySet())) { 44 | throw new ClassCastException("value Map " + map.keySet() + " is not structurally equivalent to fields " + this.fields.keySet()); 45 | } 46 | 47 | for (Map.Entry> entry : this.fields.entrySet()) { 48 | try { 49 | entry.getValue().cast(map.get(entry.getKey())); 50 | } catch (ClassCastException e) { 51 | ClassCastException ex = new ClassCastException("field " + entry.getKey()); 52 | ex.initCause(e); 53 | throw ex; 54 | } 55 | } 56 | 57 | return (Map) map; 58 | } 59 | 60 | @Override 61 | public void serialize(TypeSerializer serializer, S target) { 62 | serializer.serialize(this, target); 63 | } 64 | 65 | @Override 66 | public S serializeValue(Map value, ValueSerializer serializer) { 67 | return serializer.serializeRecord(value, this); 68 | } 69 | 70 | @Override 71 | public Map deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 72 | return serializer.deserializeRecord(elem, this); 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return new StringJoiner(", ", RecordSerializableType.class.getSimpleName() + "[", "]") 78 | .add("fields=" + this.fields.entrySet().stream() 79 | .map(entry -> entry.getKey() + ':' + entry.getValue()) 80 | .collect(Collectors.joining(", ", "{", "}"))) 81 | .toString(); 82 | } 83 | 84 | @Override 85 | public boolean equals(Object o) { 86 | if (this == o) return true; 87 | if (o == null || this.getClass() != o.getClass()) return false; 88 | RecordSerializableType that = (RecordSerializableType) o; 89 | return Objects.equals(this.fields, that.fields); 90 | } 91 | 92 | @Override 93 | public int hashCode() { 94 | return Objects.hash(this.fields); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/builder/ConfigNodeBuilder.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.builder; 2 | 3 | import java.util.Collection; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import javax.annotation.Nullable; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.FiberId; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigAttribute; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 14 | 15 | public abstract class ConfigNodeBuilder { 16 | @Nullable 17 | protected ConfigTree parent; 18 | @Nullable 19 | protected String name; 20 | @Nullable 21 | protected String comment = null; 22 | protected Map> attributes; 23 | 24 | public ConfigNodeBuilder(@Nullable ConfigTree parent, @Nullable String name) { 25 | if (parent != null && name == null) throw new IllegalArgumentException("A child node needs a name"); 26 | this.parent = parent; 27 | this.name = name; 28 | this.attributes = new HashMap<>(); 29 | } 30 | 31 | /** 32 | * Sets the {@code ConfigNode}'s name. 33 | * 34 | * @param name the name 35 | * @return {@code this} builder 36 | * @see ConfigTree#lookupLeaf 37 | */ 38 | public ConfigNodeBuilder withName(String name) { 39 | this.name = name; 40 | return this; 41 | } 42 | 43 | /** 44 | * Sets the {@code ConfigNode}'s comment. 45 | * 46 | *

If {@code null}, or if this method is never called, the {@code ConfigNode} will not have a comment. 47 | * An empty comment (non null, but only consisting of whitespace) will be serialised. 48 | * 49 | * @param comment the comment 50 | * @return {@code this} builder 51 | */ 52 | public ConfigNodeBuilder withComment(String comment) { 53 | this.comment = comment; 54 | return this; 55 | } 56 | 57 | /** 58 | * Adds a {@link ConfigAttribute} to the built {@code ConfigNode}. 59 | * 60 | * @param id the id of the attribute 61 | * @param type the class object representing the type of values stored in the attribute 62 | * @param defaultValue the attribute's default value 63 | * @param the type of values stored in the attribute 64 | * @return {@code this}, for chaining 65 | * @see ConfigNode#getAttributes() 66 | */ 67 | public ConfigNodeBuilder withAttribute(FiberId id, SerializableType type, A defaultValue) { 68 | return this.withAttribute(ConfigAttribute.create(id, type, defaultValue)); 69 | } 70 | 71 | /** 72 | * Adds a collection of {@link ConfigAttribute} to the built {@code ConfigNode}. 73 | * 74 | * @param attributes A collection of attributes. 75 | * @return This builder. 76 | */ 77 | public ConfigNodeBuilder withAttributes(Collection> attributes) { 78 | for (ConfigAttribute attribute : attributes) { 79 | this.withAttribute(attribute); 80 | } 81 | 82 | return this; 83 | } 84 | 85 | /** 86 | * Adds a single {@link ConfigAttribute} to the built {@code ConfigNode}. 87 | * 88 | * @param attribute The attribute. 89 | * @return This builder. 90 | */ 91 | public ConfigNodeBuilder withAttribute(ConfigAttribute attribute) { 92 | this.attributes.put(attribute.getIdentifier(), attribute); 93 | return this; 94 | } 95 | 96 | /** 97 | * Builds and returns a new {@code ConfigNode} with the parent and name stored in this builder. 98 | */ 99 | public abstract ConfigNode build(); 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/tree/IndexedNodeCollection.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import java.util.AbstractCollection; 4 | import java.util.Iterator; 5 | import java.util.LinkedHashMap; 6 | import java.util.Map; 7 | import java.util.Objects; 8 | import java.util.Spliterator; 9 | 10 | import javax.annotation.Nonnull; 11 | import javax.annotation.Nullable; 12 | 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.DuplicateChildException; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 16 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.NodeCollection; 17 | 18 | public class IndexedNodeCollection extends AbstractCollection implements NodeCollection { 19 | // the node ordering is important, it will be kept in the config file 20 | private final Map items = new LinkedHashMap<>(); 21 | @Nullable 22 | private final ConfigBranch owner; 23 | 24 | public IndexedNodeCollection(@Nullable ConfigBranch owner) { 25 | this.owner = owner; 26 | } 27 | 28 | @Nonnull 29 | @Override 30 | public Iterator iterator() { 31 | return new Iterator() { 32 | @Nullable 33 | private ConfigNode last; 34 | private final Iterator backing = items.values().iterator(); 35 | 36 | @Override 37 | public boolean hasNext() { 38 | return backing.hasNext(); 39 | } 40 | 41 | @Override 42 | public ConfigNode next() { 43 | this.last = this.backing.next(); 44 | return last; 45 | } 46 | 47 | @Override 48 | public void remove() { 49 | if (this.last == null) throw new IllegalStateException(); 50 | // order is important to avoid infinite recursion 51 | this.backing.remove(); 52 | this.last.detach(); 53 | } 54 | }; 55 | } 56 | 57 | @Override 58 | public Spliterator spliterator() { 59 | return this.items.values().spliterator(); 60 | } 61 | 62 | @Override 63 | public boolean add(ConfigNode item) throws DuplicateChildException { 64 | return add(item, false); 65 | } 66 | 67 | @Override 68 | public boolean add(ConfigNode item, boolean overwrite) throws DuplicateChildException { 69 | Objects.requireNonNull(item); 70 | 71 | if (overwrite) { 72 | this.removeByName(item.getName()); 73 | } else if (this.items.containsKey(item.getName())) { 74 | throw new DuplicateChildException("Attempt to replace node " + item.getName()); 75 | } 76 | 77 | this.items.put(item.getName(), item); 78 | item.attachTo(this.owner); 79 | return true; 80 | } 81 | 82 | @Override 83 | public boolean contains(@Nullable Object o) { 84 | if (o instanceof ConfigNode) { 85 | return Objects.equals(this.items.get(((ConfigNode) o).getName()), o); 86 | } 87 | 88 | return false; 89 | } 90 | 91 | @Override 92 | public boolean remove(@Nullable Object child) { 93 | if (child instanceof ConfigNode) { 94 | boolean removed = this.items.remove(((ConfigNode) child).getName(), child); 95 | 96 | if (removed) { 97 | ((ConfigNode) child).detach(); 98 | return true; 99 | } 100 | } 101 | 102 | return false; 103 | } 104 | 105 | @Override 106 | public int size() { 107 | return items.size(); 108 | } 109 | 110 | @Override 111 | public ConfigNode getByName(String name) { 112 | return this.items.get(name); 113 | } 114 | 115 | @Override 116 | @Nullable 117 | public ConfigNode removeByName(String name) { 118 | ConfigNode removed = this.items.remove(name); 119 | 120 | if (removed != null) { 121 | removed.detach(); 122 | } 123 | 124 | return removed; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/NodeCollection.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import java.util.Collection; 4 | 5 | import javax.annotation.Nullable; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.DuplicateChildException; 8 | 9 | /** 10 | * A specialized {@link Collection} implementation for use with nodes. 11 | * 12 | *

Note: This is not a general-purpose collection. 13 | * Mutating methods in this class will mutate external state. 14 | * 15 | *

Elements in a node collection are considered children of the same tree. 16 | * For this reason, each element of a node collection must have a distinct name. 17 | * Mutating methods in this class will also update the {@linkplain ConfigNode#getParent() parent} 18 | * field of added and removed elements. 19 | * 20 | *

The iterator returned by the {@code iterator} method traverses the 21 | * elements in ascending name order (ie. lexicographic order of the nodes' names). 22 | * 23 | *

Null elements are not permitted. Attempts to insert a null element 24 | * will throw a {@link NullPointerException}. Attempts to test for the 25 | * presence of a null element or to remove one will, however, function 26 | * properly. 27 | */ 28 | public interface NodeCollection extends Collection { 29 | /** 30 | * Attempts to introduce a new child to this collection. 31 | * 32 | *

This method behaves as if {@code add(node, false)}. 33 | * 34 | * @param child The child to add 35 | * @return {@code true} (as specified by {@link Collection#add}) 36 | * @throws DuplicateChildException if there was already a child by the same name 37 | * @throws IllegalStateException if the child cannot be added to this tree at this time 38 | * @throws NullPointerException if {@code node} is null 39 | */ 40 | @Override 41 | boolean add(ConfigNode child) throws DuplicateChildException; 42 | 43 | /** 44 | * Attempts to introduce a new child to this collection. 45 | * 46 | *

If this method returns normally, the {@code child} will be attached 47 | * to this collection's owner. 48 | * 49 | * @param child The child to add 50 | * @param overwrite whether existing items with the same name should be overwritten 51 | * @return {@code true} (as specified by {@link Collection#add}) 52 | * @throws DuplicateChildException if there exists a child by the same name that was not overwritten 53 | * @throws IllegalStateException if the child cannot be added to this tree at this time 54 | * @throws NullPointerException if {@code node} is null 55 | * @see ConfigNode#attachTo(ConfigBranch) 56 | */ 57 | boolean add(ConfigNode child, boolean overwrite) throws DuplicateChildException; 58 | 59 | /** 60 | * Removes a child from this collection. 61 | * 62 | *

When this method returns, the {@code child} will be detached from 63 | * this collection's owner. If the given object is not contained within this 64 | * collection, this method returns {@code false} with no side effects. 65 | * 66 | * @param child the child to remove 67 | * @return {@code true} if a node was detached as a result of this call 68 | * @see ConfigNode#detach() 69 | */ 70 | @Override 71 | boolean remove(Object child); 72 | 73 | /** 74 | * Tries to find a child in this collection by name. If a child is found, it will be returned. 75 | * 76 | * @param name The name of the child to look for 77 | * @return the child if found, otherwise {@code null} 78 | */ 79 | ConfigNode getByName(String name); 80 | 81 | /** 82 | * Attempts to remove an item from this collection by name. 83 | * 84 | * @param name the name of the child that should be removed 85 | * @return the child if removed, otherwise {@code null} 86 | */ 87 | @Nullable 88 | ConfigNode removeByName(String name); 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/serialization/FiberSerialization.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.serialization; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.Iterator; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.Commentable; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 16 | 17 | /** 18 | * Static class that houses Fiber's serialization and deserialization algorithms. 19 | */ 20 | public final class FiberSerialization { 21 | private FiberSerialization() { 22 | } 23 | 24 | public static void serialize(ConfigTree tree, OutputStream out, ValueSerializer ctx) throws IOException { 25 | T target = ctx.newTarget(); 26 | 27 | for (ConfigNode node : tree.getItems()) { 28 | serializeNode(node, target, ctx); 29 | } 30 | 31 | ctx.writeTarget(target, out); 32 | } 33 | 34 | public static void deserialize(ConfigTree tree, InputStream in, ValueSerializer ctx) throws IOException, ValueDeserializationException { 35 | T target = ctx.readTarget(in); 36 | 37 | for (Iterator> itr = ctx.elements(target); itr.hasNext(); ) { 38 | Map.Entry entry = itr.next(); 39 | ConfigNode node = tree.lookup(entry.getKey()); 40 | A elem = entry.getValue(); 41 | 42 | if (node != null) { 43 | deserializeNode(node, elem, ctx); 44 | } 45 | } 46 | } 47 | 48 | public static void serializeNode(ConfigNode node, T target, ValueSerializer ctx) { 49 | String name = Objects.requireNonNull(node.getName()); 50 | String comment; 51 | 52 | if (node instanceof Commentable) { 53 | comment = ((Commentable) node).getComment(); 54 | } else { 55 | comment = null; 56 | } 57 | 58 | if (node instanceof ConfigBranch) { 59 | ConfigBranch branch = (ConfigBranch) node; 60 | 61 | if (!branch.isSerializedSeparately()) { 62 | T subTarget = ctx.newTarget(); 63 | 64 | for (ConfigNode subNode : branch.getItems()) { 65 | serializeNode(subNode, subTarget, ctx); 66 | } 67 | 68 | ctx.addSubElement(name, subTarget, target, comment); 69 | } 70 | } else if (node instanceof ConfigLeaf) { 71 | ConfigLeaf leaf = (ConfigLeaf) node; 72 | ctx.addElement(name, serializeValue(leaf, ctx), target, comment); 73 | } 74 | } 75 | 76 | private static A serializeValue(ConfigLeaf leaf, ValueSerializer ctx) { 77 | return leaf.getConfigType().serializeValue(leaf.getValue(), ctx); 78 | } 79 | 80 | public static void deserializeNode(ConfigNode node, A elem, ValueSerializer ctx) throws ValueDeserializationException { 81 | if (node instanceof ConfigBranch) { 82 | ConfigBranch branch = (ConfigBranch) node; 83 | 84 | for (Iterator> itr = ctx.subElements(elem); itr.hasNext(); ) { 85 | Map.Entry entry = itr.next(); 86 | ConfigNode subNode = branch.lookup(entry.getKey()); 87 | A subElem = entry.getValue(); 88 | 89 | if (subNode != null) { 90 | deserializeNode(subNode, subElem, ctx); 91 | } 92 | } 93 | } else if (node instanceof ConfigLeaf) { 94 | ConfigLeaf leaf = (ConfigLeaf) node; 95 | deserializeValue(leaf, elem, ctx); 96 | } 97 | } 98 | 99 | private static void deserializeValue(ConfigLeaf leaf, A elem, ValueSerializer ctx) throws ValueDeserializationException { 100 | leaf.setValue(leaf.getConfigType().deserializeValue(elem, ctx)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/StringSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.util.Objects; 4 | import java.util.StringJoiner; 5 | import java.util.regex.Pattern; 6 | 7 | import javax.annotation.Nullable; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 12 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.StringConstraintChecker; 13 | 14 | /** 15 | * The {@link SerializableType} for regex-defined {@link String} values. 16 | */ 17 | public final class StringSerializableType extends PlainSerializableType { 18 | public static final StringSerializableType DEFAULT_STRING = new StringSerializableType(0, Integer.MAX_VALUE, null); 19 | 20 | private final int minLength; 21 | private final int maxLength; 22 | @Nullable 23 | private final Pattern pattern; 24 | 25 | public StringSerializableType(int minLength, int maxLength, @Nullable Pattern pattern) { 26 | super(String.class, StringConstraintChecker.instance()); 27 | this.minLength = minLength; 28 | this.maxLength = maxLength; 29 | this.pattern = pattern; 30 | } 31 | 32 | /** 33 | * Specifies a minimum string length. 34 | * 35 | *

Values must be of equal or longer length than the returned value to satisfy the constraint. 36 | * For example: if the min length is 3. 37 | *

41 | */ 42 | public int getMinLength() { 43 | return this.minLength; 44 | } 45 | 46 | /** 47 | * Specifies a maximum string length. 48 | * 49 | *

Values must be of equal or shorter length than the returned value to satisfy the constraint. 50 | * For example: if the max length is 3. 51 | *

    52 | *
  • {@code "AB"} and {@code "ABC"} would satisfy the constraint
  • 53 | *
  • {@code "ABCD"} would not satisfy the constraint
  • 54 | *
55 | */ 56 | public int getMaxLength() { 57 | return this.maxLength; 58 | } 59 | 60 | /** 61 | * Specifies a pattern that must match. 62 | * 63 | *

Values must match the constraint's value, which is a regular expression (regex). 64 | */ 65 | @Nullable 66 | public Pattern getPattern() { 67 | return this.pattern; 68 | } 69 | 70 | @Override 71 | public void serialize(TypeSerializer serializer, S target) { 72 | serializer.serialize(this, target); 73 | } 74 | 75 | @Override 76 | public S serializeValue(String value, ValueSerializer serializer) { 77 | return serializer.serializeString(value, this); 78 | } 79 | 80 | @Override 81 | public String deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 82 | return serializer.deserializeString(elem, this); 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) return true; 88 | if (o == null || this.getClass() != o.getClass()) return false; 89 | StringSerializableType that = (StringSerializableType) o; 90 | return this.minLength == that.minLength 91 | && this.maxLength == that.maxLength 92 | && Objects.equals(this.pattern, that.pattern); 93 | } 94 | 95 | @Override 96 | public int hashCode() { 97 | return Objects.hash(this.minLength, this.maxLength, this.pattern); 98 | } 99 | 100 | @Override 101 | public String toString() { 102 | return new StringJoiner(", ", StringSerializableType.class.getSimpleName() + "[", "]") 103 | .add("minLength=" + this.minLength) 104 | .add("maxLength=" + this.maxLength) 105 | .add("pattern=" + this.pattern) 106 | .toString(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/tree/ConfigBranchImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import java.util.Collection; 4 | import java.util.Collections; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.NodeCollection; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.PropertyMirror; 15 | 16 | /** 17 | * Class implementing {@link ConfigBranch}. 18 | */ 19 | public class ConfigBranchImpl extends ConfigNodeImpl implements ConfigBranch { 20 | private final NodeCollection items; 21 | private final boolean serializeSeparately; 22 | 23 | /** 24 | * Creates a new {@code ConfigBranch}. 25 | * 26 | * @param name the name for this {@link ConfigBranchImpl} 27 | * @param comment the comment for this {@link ConfigBranchImpl} 28 | * @param items the node's items 29 | * @param serializeSeparately whether or not this node should be serialised separately. If {@code true}, it will be ignored during serialisation. 30 | */ 31 | public ConfigBranchImpl(String name, @Nullable String comment, @Nonnull Collection items, boolean serializeSeparately) { 32 | super(name, comment); 33 | this.items = new IndexedNodeCollection(this); 34 | this.serializeSeparately = serializeSeparately; 35 | // must do 2-step initialization, to avoid leaking uninitialized 36 | this.items.addAll(items); 37 | } 38 | 39 | /** 40 | * Creates a new {@code ConfigBranch} with the provided {@code name} and {@code comment}. 41 | * 42 | *

This node will not be serialised separately. 43 | * 44 | * @param name the name for this {@link ConfigBranchImpl} 45 | * @param comment the comment for this {@link ConfigBranchImpl} 46 | */ 47 | public ConfigBranchImpl(@Nonnull String name, @Nullable String comment) { 48 | this(name, comment, Collections.emptyList(), false); 49 | } 50 | 51 | /** 52 | * Creates a new {@code ConfigBranch} without a name or comment. 53 | * 54 | *

This node will not be serialised separately. 55 | */ 56 | public ConfigBranchImpl() { 57 | this(null, null, Collections.emptyList(), false); 58 | } 59 | 60 | @Nonnull 61 | @Override 62 | public NodeCollection getItems() { 63 | return items; 64 | } 65 | 66 | @Nullable 67 | @Override 68 | public ConfigNode lookup(String name) { 69 | return this.items.getByName(name); 70 | } 71 | 72 | @Nullable 73 | @Override 74 | public ConfigLeaf lookupLeaf(String name, SerializableType type) { 75 | ConfigNode child = this.items.getByName(name); 76 | 77 | if (child instanceof ConfigLeaf && type.isAssignableFrom(((ConfigLeaf) child).getConfigType())) { 78 | @SuppressWarnings("unchecked") ConfigLeaf leaf = (ConfigLeaf) child; 79 | return leaf; 80 | } 81 | 82 | return null; 83 | } 84 | 85 | @Override 86 | public boolean lookupAndBind(String name, PropertyMirror mirror) { 87 | ConfigLeaf leaf = this.lookupLeaf(name, mirror.getMirroredType().getSerializedType()); 88 | 89 | if (leaf != null) { 90 | mirror.mirror(leaf); 91 | return true; 92 | } 93 | 94 | return false; 95 | } 96 | 97 | @Nullable 98 | @Override 99 | public ConfigBranch lookupBranch(String name) { 100 | ConfigNode child = this.items.getByName(name); 101 | 102 | if (child instanceof ConfigBranch) { 103 | return (ConfigBranch) child; 104 | } 105 | 106 | return null; 107 | } 108 | 109 | @Override 110 | public boolean isSerializedSeparately() { 111 | return serializeSeparately; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/ListSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.StringJoiner; 7 | 8 | import javax.annotation.Nonnull; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 13 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.ListConstraintChecker; 14 | 15 | /** 16 | * The {@link SerializableType} for a generic {@link List}. 17 | * 18 | * @param The type of elements objects of this type hold. 19 | */ 20 | public final class ListSerializableType extends ParameterizedSerializableType> { 21 | private final SerializableType elementType; 22 | private final boolean unique; 23 | private final int minSize; 24 | private final int maxSize; 25 | 26 | public ListSerializableType(SerializableType elementType) { 27 | this(elementType, 0, Integer.MAX_VALUE, false); 28 | } 29 | 30 | public ListSerializableType(SerializableType elementType, int minSize, int maxSize, boolean unique) { 31 | super(List.class, ListConstraintChecker.instance()); 32 | this.elementType = elementType; 33 | this.minSize = minSize; 34 | this.maxSize = maxSize; 35 | this.unique = unique; 36 | } 37 | 38 | public SerializableType getElementType() { 39 | return this.elementType; 40 | } 41 | 42 | public int getMinSize() { 43 | return this.minSize; 44 | } 45 | 46 | public int getMaxSize() { 47 | return this.maxSize; 48 | } 49 | 50 | public boolean hasUniqueElements() { 51 | return this.unique; 52 | } 53 | 54 | @Override 55 | public ParameterizedType getParameterizedType() { 56 | return new ParameterizedTypeImpl(this.getErasedPlatformType(), this.elementType.getGenericPlatformType()); 57 | } 58 | 59 | @SuppressWarnings("unchecked") 60 | @Override 61 | public List cast(@Nonnull Object value) { 62 | List ls = (List) value; 63 | 64 | for (Object obj : ls) { 65 | try { 66 | this.elementType.cast(obj); 67 | } catch (ClassCastException e) { 68 | ClassCastException ex = new ClassCastException("element " + obj); 69 | ex.initCause(e); 70 | throw ex; 71 | } 72 | } 73 | 74 | return (List) ls; 75 | } 76 | 77 | @Override 78 | public void serialize(TypeSerializer serializer, S target) { 79 | serializer.serialize(this, target); 80 | } 81 | 82 | @Override 83 | public S serializeValue(List value, ValueSerializer serializer) { 84 | return serializer.serializeList(value, this); 85 | } 86 | 87 | @Override 88 | public List deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 89 | return serializer.deserializeList(elem, this); 90 | } 91 | 92 | @Override 93 | public boolean equals(Object o) { 94 | if (this == o) return true; 95 | if (o == null || this.getClass() != o.getClass()) return false; 96 | ListSerializableType that = (ListSerializableType) o; 97 | return this.unique == that.unique 98 | && this.minSize == that.minSize 99 | && this.maxSize == that.maxSize 100 | && Objects.equals(this.elementType, that.elementType); 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return Objects.hash(this.elementType, this.unique, this.minSize, this.maxSize); 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return new StringJoiner(", ", ListSerializableType.class.getSimpleName() + "<" + this.elementType + ">" + "[", "]") 111 | .add("unique=" + unique) 112 | .add("minSize=" + minSize) 113 | .add("maxSize=" + maxSize) 114 | .toString(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/exception/FiberQueryException.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.exception; 2 | 3 | import javax.annotation.Nonnull; 4 | import javax.annotation.Nullable; 5 | 6 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigQuery; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 11 | 12 | /** 13 | * Signals that an exception occurred while running a {@link ConfigQuery}. 14 | * 15 | *

This class is the general class of exceptions produced by failed 16 | * config tree queries. 17 | */ 18 | public class FiberQueryException extends FiberException { 19 | private final ConfigTree invalidTree; 20 | 21 | public FiberQueryException(String message, ConfigTree invalidTree) { 22 | super(message + (invalidTree instanceof ConfigBranch && ((ConfigBranch) invalidTree).getName() != null 23 | ? " (in branch " + ((ConfigBranch) invalidTree).getName() + ")" : "")); 24 | this.invalidTree = invalidTree; 25 | } 26 | 27 | public FiberQueryException(String message, Throwable cause, ConfigTree invalidTree) { 28 | super(message, cause); 29 | this.invalidTree = invalidTree; 30 | } 31 | 32 | /** 33 | * Returns the last valid ancestor before which the error occurred. 34 | * 35 | *

The invalid tree may be the tree directly passed to the query, 36 | * or it may be a descendant node. 37 | * 38 | * @return the parent of the erroring node. 39 | */ 40 | public ConfigTree getErrorParent() { 41 | return this.invalidTree; 42 | } 43 | 44 | /** 45 | * Checked exception thrown when a query fails to find a child 46 | * with a given name from an ancestor node. 47 | */ 48 | public static class MissingChild extends FiberQueryException { 49 | private final String missingNodeName; 50 | 51 | public MissingChild(String name, ConfigTree invalidTree) { 52 | super("Missing child " + name, invalidTree); 53 | this.missingNodeName = name; 54 | } 55 | 56 | /** 57 | * Returns the name of the missing child. 58 | * 59 | * @return the name of the missing child 60 | */ 61 | @Nonnull 62 | public String getMissingChildName() { 63 | return this.missingNodeName; 64 | } 65 | } 66 | 67 | /** 68 | * Checked exception thrown when a query finds a node of 69 | * a different type than expected. 70 | */ 71 | public static class WrongType extends FiberQueryException { 72 | private final ConfigNode invalidItem; 73 | private final Class expectedNodeType; 74 | @Nullable 75 | private final SerializableType expectedValueType; 76 | 77 | public WrongType(ConfigTree invalidTree, ConfigNode invalidItem, Class expectedNodeType, @Nullable SerializableType expectedValueType) { 78 | super("Expected node of type " + expectedNodeType.getSimpleName() 79 | + (expectedValueType == null ? "" : "<" + expectedValueType + ">") 80 | + ", got " + invalidItem, invalidTree); 81 | this.invalidItem = invalidItem; 82 | this.expectedNodeType = expectedNodeType; 83 | this.expectedValueType = expectedValueType; 84 | } 85 | 86 | /** 87 | * The actual node found, which is of unexpected type. 88 | */ 89 | public ConfigNode getInvalidNode() { 90 | return this.invalidItem; 91 | } 92 | 93 | /** 94 | * The expected type given in the config query. 95 | */ 96 | public Class getExpectedNodeType() { 97 | return this.expectedNodeType; 98 | } 99 | 100 | /** 101 | * Returns the type of property values expected by the query. 102 | * 103 | *

If the query expected an ancestor node to be found, this method returns {@code null}. 104 | * 105 | * @return the expected value type, or {@code null} if the query did not expect a property 106 | */ 107 | @Nullable 108 | public SerializableType getExpectedValueType() { 109 | return this.expectedValueType; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/DecimalSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Objects; 5 | import java.util.StringJoiner; 6 | 7 | import javax.annotation.Nullable; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 12 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.DecimalConstraintChecker; 13 | 14 | /** 15 | * The {@link SerializableType} for numeric ranges. This type handles integral as well as real 16 | * and fractional ranges using Java's {@link BigDecimal} type. 17 | */ 18 | public final class DecimalSerializableType extends PlainSerializableType { 19 | /** 20 | * Specifies a numerical lower bound. 21 | * 22 | *

Values must be equal to or greater than the constraint's value to satisfy the constraint. 23 | */ 24 | @Nullable 25 | private final BigDecimal minimum; 26 | /** 27 | * Specifies a numerical upper bound. 28 | * 29 | *

Values must be equal to or lesser than the constraint's value to satisfy the constraint. 30 | */ 31 | @Nullable 32 | private final BigDecimal maximum; 33 | @Nullable 34 | private final BigDecimal increment; 35 | 36 | public DecimalSerializableType(@Nullable BigDecimal min, @Nullable BigDecimal max, @Nullable BigDecimal increment) { 37 | super(BigDecimal.class, DecimalConstraintChecker.instance()); 38 | 39 | if (min != null && max != null) { 40 | if (min.compareTo(max) > 0) { 41 | throw new IllegalArgumentException("Provided minimum " + min + " is greater than provided maximum " + max); 42 | } 43 | 44 | if (increment != null && max.subtract(min).compareTo(increment) < 0) { 45 | throw new IllegalArgumentException("Provided step " + increment + " is bigger than range [" + min + ", " + max + "]"); 46 | } 47 | } 48 | 49 | if (increment != null) { 50 | if (min == null) throw new IllegalStateException("A nonnull increment requires a minimum value"); 51 | 52 | if (increment.compareTo(BigDecimal.ZERO) <= 0) { 53 | throw new IllegalArgumentException("Increment cannot be negative (" + increment + ")"); 54 | } 55 | } 56 | 57 | this.minimum = min; 58 | this.maximum = max; 59 | this.increment = increment; 60 | } 61 | 62 | @Nullable 63 | public BigDecimal getMinimum() { 64 | return this.minimum; 65 | } 66 | 67 | @Nullable 68 | public BigDecimal getMaximum() { 69 | return this.maximum; 70 | } 71 | 72 | @Nullable 73 | public BigDecimal getIncrement() { 74 | return this.increment; 75 | } 76 | 77 | @Override 78 | public void serialize(TypeSerializer serializer, S target) { 79 | serializer.serialize(this, target); 80 | } 81 | 82 | @Override 83 | public S serializeValue(BigDecimal value, ValueSerializer serializer) { 84 | return serializer.serializeNumber(value, this); 85 | } 86 | 87 | @Override 88 | public BigDecimal deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 89 | return serializer.deserializeNumber(elem, this); 90 | } 91 | 92 | @Override 93 | public boolean equals(Object o) { 94 | if (this == o) return true; 95 | if (o == null || this.getClass() != o.getClass()) return false; 96 | DecimalSerializableType that = (DecimalSerializableType) o; 97 | return Objects.equals(this.minimum, that.minimum) 98 | && Objects.equals(this.maximum, that.maximum) 99 | && Objects.equals(this.increment, that.increment); 100 | } 101 | 102 | @Override 103 | public int hashCode() { 104 | return Objects.hash(this.minimum, this.maximum, this.increment); 105 | } 106 | 107 | @Override 108 | public String toString() { 109 | return new StringJoiner(", ", DecimalSerializableType.class.getSimpleName() + "[", "]") 110 | .add("minimum=" + minimum) 111 | .add("maximum=" + maximum) 112 | .add("increment=" + increment) 113 | .toString(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/MapSerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.ParameterizedType; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.StringJoiner; 7 | 8 | import javax.annotation.Nonnull; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 13 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.MapConstraintChecker; 14 | 15 | /** 16 | * The {@link SerializableType} for mappings from {@link String} keys to some value type. 17 | * 18 | * @param The serialized value type. 19 | */ 20 | public final class MapSerializableType extends ParameterizedSerializableType> { 21 | private final StringSerializableType keyType; 22 | private final SerializableType valueType; 23 | private final int minSize; 24 | private final int maxSize; 25 | 26 | public MapSerializableType(SerializableType valueType) { 27 | this(StringSerializableType.DEFAULT_STRING, valueType); 28 | } 29 | 30 | public MapSerializableType(StringSerializableType keyType, SerializableType valueType) { 31 | this(keyType, valueType, 0, Integer.MAX_VALUE); 32 | } 33 | 34 | public MapSerializableType(StringSerializableType keyType, SerializableType valueType, int minSize, int maxSize) { 35 | super(Map.class, MapConstraintChecker.instance()); 36 | this.keyType = keyType; 37 | this.valueType = valueType; 38 | this.minSize = minSize; 39 | this.maxSize = maxSize; 40 | } 41 | 42 | public StringSerializableType getKeyType() { 43 | return this.keyType; 44 | } 45 | 46 | public SerializableType getValueType() { 47 | return this.valueType; 48 | } 49 | 50 | public int getMinSize() { 51 | return this.minSize; 52 | } 53 | 54 | public int getMaxSize() { 55 | return this.maxSize; 56 | } 57 | 58 | @Override 59 | public ParameterizedType getParameterizedType() { 60 | return new ParameterizedTypeImpl(this.getErasedPlatformType(), String.class, this.valueType.getGenericPlatformType()); 61 | } 62 | 63 | @SuppressWarnings("unchecked") 64 | @Override 65 | public Map cast(@Nonnull Object value) { 66 | Map map = (Map) value; 67 | 68 | for (Map.Entry entry : map.entrySet()) { 69 | if (!(entry.getKey() instanceof String)) { 70 | throw new ClassCastException("non-String map key " + entry.getKey()); 71 | } 72 | 73 | try { 74 | this.valueType.cast(entry.getValue()); 75 | } catch (ClassCastException e) { 76 | ClassCastException ex = new ClassCastException("map value " + entry.getValue()); 77 | ex.initCause(e); 78 | throw ex; 79 | } 80 | } 81 | 82 | return (Map) map; 83 | } 84 | 85 | @Override 86 | public void serialize(TypeSerializer serializer, S target) { 87 | serializer.serialize(this, target); 88 | } 89 | 90 | @Override 91 | public S serializeValue(Map value, ValueSerializer serializer) { 92 | return serializer.serializeMap(value, this); 93 | } 94 | 95 | @Override 96 | public Map deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException { 97 | return serializer.deserializeMap(elem, this); 98 | } 99 | 100 | @Override 101 | public boolean equals(Object o) { 102 | if (this == o) return true; 103 | if (o == null || this.getClass() != o.getClass()) return false; 104 | MapSerializableType that = (MapSerializableType) o; 105 | return this.minSize == that.minSize 106 | && this.maxSize == that.maxSize 107 | && Objects.equals(this.valueType, that.valueType); 108 | } 109 | 110 | @Override 111 | public int hashCode() { 112 | return Objects.hash(this.valueType, this.minSize, this.maxSize); 113 | } 114 | 115 | @Override 116 | public String toString() { 117 | return new StringJoiner(", ", MapSerializableType.class.getSimpleName() + "[", "]") 118 | .add("minSize=" + minSize) 119 | .add("maxSize=" + maxSize) 120 | .toString(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/test/java/io/github/fablabsmc/fablabs/impl/fiber/builder/constraint/ConstraintsTest.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.builder.constraint; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | 6 | import java.math.BigDecimal; 7 | import java.util.Arrays; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigLeafBuilder; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.builder.ConfigTreeBuilder; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigTypes; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ListConfigType; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.NumberConfigType; 16 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigLeaf; 17 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 18 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.PropertyMirror; 19 | import org.junit.jupiter.api.DisplayName; 20 | import org.junit.jupiter.api.Test; 21 | 22 | class ConstraintsTest { 23 | @DisplayName("Test numerical constraints") 24 | @Test 25 | public void testNumericalConstraints() { 26 | NumberConfigType type = ConfigTypes.INTEGER.withMinimum(5); 27 | 28 | ConfigLeaf leaf = ConfigLeafBuilder 29 | .create(null, "", type.getSerializedType(), BigDecimal.valueOf(5)) 30 | .build(); 31 | 32 | assertFalse(leaf.accepts(BigDecimal.valueOf(-2)), "Input can't be lower than 5"); 33 | assertFalse(leaf.accepts(BigDecimal.valueOf(4)), "Input can't be lower than 5"); 34 | 35 | assertTrue(leaf.accepts(BigDecimal.valueOf(7)), "Input can be between 5 and 10"); 36 | assertTrue(leaf.accepts(BigDecimal.valueOf(25)), "Input can be above 20"); 37 | } 38 | 39 | @DisplayName("Test array aggregate constraints") 40 | @Test 41 | public void testArrayConstraints() { 42 | ListConfigType type = ConfigTypes.makeArray( 43 | ConfigTypes.INTEGER.withValidRange(3, 10, 1) 44 | ).withMaxSize(3).withMinSize(1); 45 | ConfigLeaf> config = ConfigLeafBuilder 46 | .create(null, "foo", type, new Integer[] { 3, 10 }) 47 | .build(); 48 | PropertyMirror mirror = PropertyMirror.create(type); 49 | mirror.mirror(config); 50 | 51 | assertFalse(mirror.setValue(new Integer[0]), "unrecoverable size issue"); 52 | assertFalse(mirror.accepts(new Integer[0]), "invalid size"); 53 | assertTrue(mirror.setValue(new Integer[] {4, 5, 6}), "valid array"); 54 | assertTrue(mirror.accepts(new Integer[] {4, 5, 6}), "valid array"); 55 | assertTrue(mirror.setValue(new Integer[] {1, 2}), "recoverable elements"); 56 | assertFalse(mirror.accepts(new Integer[] {1, 2}), "invalid elements"); 57 | assertTrue(mirror.setValue(new Integer[] {5, 6, 7, 8}), "recoverable size"); 58 | assertFalse(mirror.accepts(new Integer[] {5, 6, 7, 8}), "invalid size"); 59 | assertTrue(mirror.setValue(new Integer[] {9, 10, 11}), "recoverable elements"); 60 | assertFalse(mirror.accepts(new Integer[] {9, 10, 11}), "invalid elements"); 61 | } 62 | 63 | @DisplayName("Test collection aggregate constraints") 64 | @Test 65 | public void testCollectionConstraints() { 66 | ConfigTreeBuilder builder = ConfigTree.builder(); 67 | 68 | ListConfigType, BigDecimal> type = ConfigTypes.makeList(ConfigTypes.INTEGER.withMinimum(3).withMaximum(10)).withMaxSize(3); 69 | ConfigLeaf config = builder.beginValue( 70 | "", 71 | type, 72 | Collections.singletonList(4) 73 | ).build(); 74 | PropertyMirror> mirror = PropertyMirror.create(type); 75 | mirror.mirror(config); 76 | 77 | assertTrue(mirror.setValue(Collections.emptyList())); 78 | assertTrue(mirror.accepts(Collections.emptyList())); 79 | assertTrue(mirror.setValue(Arrays.asList(4, 5, 6))); 80 | assertTrue(mirror.accepts(Arrays.asList(4, 5, 6))); 81 | assertTrue(mirror.setValue(Arrays.asList(1, 2))); 82 | assertFalse(mirror.accepts(Arrays.asList(1, 2))); 83 | assertTrue(mirror.setValue(Arrays.asList(5, 6, 7, 8))); 84 | assertFalse(mirror.accepts(Arrays.asList(5, 6, 7, 8))); 85 | assertTrue(mirror.setValue(Arrays.asList(9, 10, 11))); 86 | assertFalse(mirror.accepts(Arrays.asList(9, 10, 11))); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/serialization/JsonTypeSerializer.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.serialization; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.Map; 5 | import java.util.regex.Pattern; 6 | 7 | import blue.endless.jankson.JsonArray; 8 | import blue.endless.jankson.JsonObject; 9 | import blue.endless.jankson.JsonPrimitive; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.BooleanSerializableType; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.DecimalSerializableType; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.EnumSerializableType; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.ListSerializableType; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.MapSerializableType; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.RecordSerializableType; 16 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 17 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.StringSerializableType; 18 | 19 | /** 20 | * A {@link TypeSerializer} for the JSON serialization form. 21 | * 22 | * @see json.org 23 | */ 24 | public class JsonTypeSerializer implements TypeSerializer { 25 | @Override 26 | public void serialize(BooleanSerializableType type, JsonObject json) { 27 | json.put("type", new JsonPrimitive("boolean")); 28 | } 29 | 30 | @Override 31 | public void serialize(DecimalSerializableType type, JsonObject json) { 32 | json.put("type", new JsonPrimitive("number")); 33 | BigDecimal min = type.getMinimum(); 34 | 35 | if (min != null) { 36 | json.put("min", new JsonPrimitive(min)); 37 | } 38 | 39 | BigDecimal maximum = type.getMaximum(); 40 | 41 | if (maximum != null) { 42 | json.put("max", new JsonPrimitive(maximum)); 43 | } 44 | 45 | BigDecimal increment = type.getIncrement(); 46 | 47 | if (increment != null) { 48 | json.put("increment", new JsonPrimitive(increment)); 49 | } 50 | } 51 | 52 | @Override 53 | public void serialize(EnumSerializableType type, JsonObject json) { 54 | json.put("type", new JsonPrimitive("enum")); 55 | JsonArray values = new JsonArray(); 56 | 57 | for (String value : type.getValidValues()) { 58 | values.add(new JsonPrimitive(value)); 59 | } 60 | 61 | json.put("values", values); 62 | } 63 | 64 | @Override 65 | public void serialize(ListSerializableType type, JsonObject json) { 66 | json.put("type", new JsonPrimitive("list")); 67 | JsonObject elementType = new JsonObject(); 68 | type.getElementType().serialize(this, elementType); 69 | json.put("elementType", elementType); 70 | json.put("unique", new JsonPrimitive(type.hasUniqueElements())); 71 | json.put("minSize", new JsonPrimitive(type.getMinSize())); 72 | json.put("maxSize", new JsonPrimitive(type.getMaxSize())); 73 | } 74 | 75 | @Override 76 | public void serialize(MapSerializableType type, JsonObject json) { 77 | json.put("type", new JsonPrimitive("map")); 78 | JsonObject valueType = new JsonObject(); 79 | type.getValueType().serialize(this, valueType); 80 | json.put("valueType", valueType); 81 | json.put("minSize", new JsonPrimitive(type.getMinSize())); 82 | json.put("maxSize", new JsonPrimitive(type.getMaxSize())); 83 | } 84 | 85 | @Override 86 | public void serialize(RecordSerializableType type, JsonObject json) { 87 | json.put("type", new JsonPrimitive("record")); 88 | JsonArray fields = new JsonArray(); 89 | 90 | for (Map.Entry> entry : type.getFields().entrySet()) { 91 | JsonObject field = new JsonObject(); 92 | field.put("name", new JsonPrimitive(entry.getKey())); 93 | JsonObject fieldType = new JsonObject(); 94 | entry.getValue().serialize(this, fieldType); 95 | field.put("type", fieldType); 96 | fields.add(field); 97 | } 98 | 99 | json.put("fields", fields); 100 | } 101 | 102 | @Override 103 | public void serialize(StringSerializableType type, JsonObject json) { 104 | json.put("type", new JsonPrimitive("string")); 105 | json.put("minLength", new JsonPrimitive(type.getMinLength())); 106 | json.put("maxLength", new JsonPrimitive(type.getMaxLength())); 107 | Pattern pattern = type.getPattern(); 108 | 109 | if (pattern != null) { 110 | json.put("pattern", new JsonPrimitive(pattern.toString())); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/processor/ConstraintAnnotationProcessor.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.reflect.AnnotatedElement; 6 | 7 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.AnnotatedSettings; 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.BooleanConfigType; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.EnumConfigType; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ListConfigType; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.MapConfigType; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.NumberConfigType; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.RecordConfigType; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.StringConfigType; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigTree; 16 | 17 | /** 18 | * An annotation processor for constraints on config types. 19 | * 20 | *

Annotations made for this type of processor should 21 | * specifically target {@link ElementType#TYPE_USE}. 22 | * 23 | * @param the type of annotations processed 24 | * @see AnnotatedSettings.Builder#registerConstraintProcessor(Class, ConstraintAnnotationProcessor) 25 | */ 26 | public interface ConstraintAnnotationProcessor { 27 | /** 28 | * Called for every type use site (field or generics) that has an annotation of type {@code A}. 29 | * 30 | * @param the type of values processed 31 | * @param baseType the type being constrained 32 | * @param annotation the annotation present on the {@code annotated} element 33 | * @param annotated an annotated type use site declared in {@code pojo}'s class 34 | * @see AnnotatedSettings#applyToNode(ConfigTree, Object) 35 | */ 36 | default NumberConfigType processDecimal(NumberConfigType baseType, A annotation, AnnotatedElement annotated) { 37 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType().getSimpleName() + " for type " + baseType); 38 | } 39 | 40 | /** 41 | * Called for every type use site (field or generics) that has an annotation of type {@code A}. 42 | * 43 | * @param the type of values processed 44 | * @param baseType the type being constrained 45 | * @param annotation the annotation present on the {@code annotated} element 46 | * @param annotated an annotated type use site declared in {@code pojo}'s class 47 | * @see AnnotatedSettings#applyToNode(ConfigTree, Object) 48 | */ 49 | default StringConfigType processString(StringConfigType baseType, A annotation, AnnotatedElement annotated) { 50 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType() + " for type " + baseType); 51 | } 52 | 53 | /** 54 | * Called for every type use site (field or generics) that has an annotation of type {@code A}. 55 | * 56 | * @param the type of values processed 57 | * @param baseType the type being constrained 58 | * @param annotation the annotation present on the {@code annotated} element 59 | * @param annotated an annotated type use site declared in {@code pojo}'s class 60 | * @see AnnotatedSettings#applyToNode(ConfigTree, Object) 61 | */ 62 | default ListConfigType processList(ListConfigType baseType, A annotation, AnnotatedElement annotated) { 63 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType() + " for type " + baseType); 64 | } 65 | 66 | default EnumConfigType processEnum(EnumConfigType baseType, A annotation, AnnotatedElement annotated) { 67 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType() + " for type " + baseType); 68 | } 69 | 70 | default RecordConfigType processRecord(RecordConfigType baseType, A annotation, AnnotatedElement annotated) { 71 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType() + " for type " + baseType); 72 | } 73 | 74 | default BooleanConfigType processBoolean(BooleanConfigType baseType, A annotation, AnnotatedElement annotated) { 75 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType() + " for type " + baseType); 76 | } 77 | 78 | default MapConfigType processMap(MapConfigType baseType, A annotation, AnnotatedElement annotated) { 79 | throw new UnsupportedOperationException("Invalid annotation " + annotation.annotationType() + " for type " + baseType); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/impl/fiber/tree/ConfigNodeImpl.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.impl.fiber.tree; 2 | 3 | import java.util.Comparator; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.TreeMap; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.FiberId; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.IllegalTreeStateException; 13 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 14 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 15 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.Commentable; 16 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigAttribute; 17 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 18 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigNode; 19 | 20 | /** 21 | * A commentable node. 22 | * 23 | * @see ConfigNode 24 | * @see ConfigBranchImpl 25 | * @see ConfigLeafImpl 26 | */ 27 | public abstract class ConfigNodeImpl implements ConfigNode, Commentable { 28 | private final Map> attributes; 29 | @Nonnull 30 | private final String name; 31 | @Nullable 32 | private final String comment; 33 | @Nullable 34 | private ConfigBranch parent; 35 | 36 | /** 37 | * Creates a new {@code ConfigLeaf}. 38 | * 39 | * @param name the name for this leaf 40 | * @param comment the comment for this leaf 41 | */ 42 | public ConfigNodeImpl(@Nonnull String name, @Nullable String comment) { 43 | this.attributes = new TreeMap<>(Comparator.comparing(FiberId::toString)); 44 | this.name = name; 45 | this.comment = comment; 46 | } 47 | 48 | @Override 49 | @Nonnull 50 | public String getName() { 51 | return name; 52 | } 53 | 54 | @Override 55 | @Nullable 56 | public String getComment() { 57 | return comment; 58 | } 59 | 60 | @Nullable 61 | @Override 62 | public ConfigBranch getParent() { 63 | return this.parent; 64 | } 65 | 66 | @Override 67 | public Map> getAttributes() { 68 | return this.attributes; 69 | } 70 | 71 | @SuppressWarnings("unchecked") 72 | @Override 73 | public ConfigAttribute getOrCreateAttribute(FiberId id, SerializableType attributeType, @Nullable A defaultValue) { 74 | ConfigAttribute attr = this.getAttributes().computeIfAbsent(id, i -> ConfigAttribute.create(i, attributeType, defaultValue)); 75 | checkAttributeType(attributeType, attr); 76 | return (ConfigAttribute) attr; 77 | } 78 | 79 | @Override 80 | public Optional getAttributeValue(FiberId id, SerializableType expectedType) { 81 | ConfigAttribute attr = this.attributes.get(id); 82 | 83 | if (attr != null) { 84 | checkAttributeType(expectedType, attr); 85 | A a = expectedType.cast(attr.getValue()); 86 | return Optional.of(a); 87 | } 88 | 89 | return Optional.empty(); 90 | } 91 | 92 | @Override 93 | public Optional getAttributeValue(FiberId id, ConfigType type) { 94 | return this.getAttributeValue(id, type.getSerializedType()).map(type::toRuntimeType); 95 | } 96 | 97 | private static void checkAttributeType(SerializableType expectedType, ConfigAttribute attr) { 98 | if (!expectedType.equals(attr.getConfigType())) { 99 | throw new ClassCastException("Attempt to retrieve a value of type " + expectedType + " from attribute with type " + attr.getConfigType()); 100 | } 101 | } 102 | 103 | @Override 104 | public void detach() { 105 | // Note: infinite recursion between ConfigNode#detach() and NodeCollection#remove() could occur here, 106 | // but the latter performs the actual collection removal before detaching 107 | if (this.parent != null) { 108 | // here, we also need to avoid triggering a ConcurrentModificationException 109 | // thankfully, remove does not cause a CME if it's a no-op 110 | this.parent.getItems().remove(this); 111 | } 112 | 113 | this.parent = null; 114 | } 115 | 116 | @Override 117 | public void attachTo(ConfigBranch parent) { 118 | if (this.parent != null && this.parent != parent) { 119 | throw new IllegalTreeStateException(this + " needs to be detached before changing the parent"); 120 | } 121 | 122 | // this node may have already been added by the collection 123 | if (parent != null && !parent.getItems().contains(this)) { 124 | parent.getItems().add(this); 125 | } 126 | 127 | this.parent = parent; 128 | } 129 | 130 | @Override 131 | public String toString() { 132 | return getClass().getSimpleName() + "[name=" + getName() + ", comment=" + getComment() + "]"; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/ConfigNode.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import java.util.Map; 4 | import java.util.Optional; 5 | 6 | import javax.annotation.Nullable; 7 | 8 | import io.github.fablabsmc.fablabs.api.fiber.v1.FiberId; 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.DuplicateChildException; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.IllegalTreeStateException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 13 | 14 | /** 15 | * The building block of a tree: every node implement this interface. 16 | */ 17 | public interface ConfigNode { 18 | /** 19 | * Returns this node's name. 20 | * 21 | * @return this node's name 22 | */ 23 | String getName(); 24 | 25 | /** 26 | * Returns the map describing the attributes of this node. 27 | * 28 | *

Attributes store metadata pertaining to the node itself, rather than its value. 29 | * The returned map can be mutated by third parties to supplement the default node metadata. 30 | * Examples of attributes include translation keys or rendering information. 31 | * 32 | *

As the returned data structure is shared by every attribute source, 33 | * attributes should be grouped by namespace. 34 | * 35 | * @return this node's configurable attributes 36 | */ 37 | Map> getAttributes(); 38 | 39 | /** 40 | * Retrieves the value of the attribute with the given id and converts it to a runtime type. 41 | * 42 | * @param id the attribute's id 43 | * @param type a {@code ConfigType} describing the type of values expected and 44 | * how to convert them to the desired runtime type 45 | * @param the runtime type to which the attribute value will be converted 46 | * @param the type of values expected from the attribute 47 | * @return an {@code Optional} describing the value of the attribute, 48 | * or an empty {@code Optional} if the attribute does not exist 49 | * @throws ClassCastException if the attribute exists but has a type that is not assignable to {@code type} 50 | */ 51 | Optional getAttributeValue(FiberId id, ConfigType type); 52 | 53 | /** 54 | * Retrieves the value of the attribute with the given id. 55 | * 56 | * @param id the attribute's id 57 | * @param expectedType a {@code SerializableType} describing the type of values expected 58 | * @param the type of values expected from the attribute 59 | * @return an {@code Optional} describing the value of the attribute, 60 | * or an empty {@code Optional} if the attribute does not exist 61 | * @throws ClassCastException if the attribute exists but has a type that is not assignable to {@code expectedType} 62 | */ 63 | Optional getAttributeValue(FiberId id, SerializableType expectedType); 64 | 65 | /** 66 | * Retrieves the attribute with the given id. If it does not exist, one is created with the given type and default value. 67 | * 68 | * @param the type of value stored by the attribute 69 | * @param id the id of the desired attribute 70 | * @param attributeType the type of values held by the attribute 71 | * @param defaultValue the default value, used if the attribute does not exist 72 | * @return the current (existing or computed) attribute associated with the given id 73 | * @see #getAttributes() 74 | */ 75 | ConfigAttribute getOrCreateAttribute(FiberId id, SerializableType attributeType, @Nullable A defaultValue); 76 | 77 | /** 78 | * Returns this node's parent, if any. 79 | * 80 | * @return this node's parent, or {@code null} if it is not part of a config tree 81 | */ 82 | @Nullable 83 | ConfigBranch getParent(); 84 | 85 | /** 86 | * Attaches this node to an existing branch. 87 | * 88 | *

After this method has returned normally, this node will be part 89 | * of the branch's {@linkplain ConfigBranch#getItems() children}, and this 90 | * node's parent will be set to {@code parent}. 91 | * 92 | *

If {@code parent} is {@code null}, this method does not mutate any state. 93 | * It will however still throw {@code IllegalTreeStateException} if this node 94 | * is not in a suitable state to be attached to another parent. To detach the node 95 | * from its current parent, use {@link #detach()}. 96 | * 97 | * @param parent the new parent branch for this node 98 | * @throws DuplicateChildException if the new parent already has a child of the same name 99 | * @throws IllegalTreeStateException if this node is already attached to another branch 100 | * @see NodeCollection#add(ConfigNode, boolean) 101 | */ 102 | void attachTo(ConfigBranch parent) throws DuplicateChildException, IllegalTreeStateException; 103 | 104 | /** 105 | * Detaches this node from its parent branch, if any. 106 | * 107 | *

After this method has returned, this node will be removed from 108 | * the current parent's {@linkplain ConfigBranch#getItems() children}, and this 109 | * node's parent will be set to {@code null}. 110 | * 111 | *

This method has no effect is this node has no parent. 112 | * 113 | * @see NodeCollection#remove(Object) 114 | */ 115 | void detach(); 116 | } 117 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/SerializableType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type; 2 | 3 | import java.lang.reflect.Type; 4 | import java.math.BigDecimal; 5 | import java.util.Objects; 6 | 7 | import javax.annotation.Nonnull; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.ValueDeserializationException; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived.ConfigType; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.TypeSerializer; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.serialization.ValueSerializer; 13 | import io.github.fablabsmc.fablabs.impl.fiber.constraint.ConstraintChecker; 14 | 15 | /** 16 | * A data type that is convertible to a config primitive. 17 | * 18 | *

A {@code SerializableType} constrains the range of possible values 19 | * through additional properties. This means that for each kind of 20 | * {@code SerializableType} there may be an arbitrary amount of different 21 | * sets of valid values, all being mapped to the same platform type. 22 | * For example, {@link Integer} and {@link Double} are both represented 23 | * as {@link DecimalSerializableType}s and stored as {@link BigDecimal}s, 24 | * but with different constraints. 25 | * 26 | * @param the actual platform type used to store the serializable data 27 | * @see ConfigType 28 | * @see BooleanSerializableType 29 | * @see EnumSerializableType 30 | * @see ListSerializableType 31 | * @see MapSerializableType 32 | * @see DecimalSerializableType 33 | * @see RecordSerializableType 34 | * @see StringSerializableType 35 | */ 36 | public abstract class SerializableType { 37 | private final Class platformType; 38 | private final ConstraintChecker> checker; 39 | 40 | @SuppressWarnings("unchecked") 41 | SerializableType(Class platformType, ConstraintChecker> checker) { 42 | this.platformType = platformType; 43 | this.checker = (ConstraintChecker>) checker; 44 | } 45 | 46 | /** 47 | * The (erased) Java platform type used to represent values of this type. 48 | */ 49 | public Class getErasedPlatformType() { 50 | return this.platformType; 51 | } 52 | 53 | /** 54 | * The generic Java platform type used to represent values of this type, with parameterized 55 | * type information preserved. 56 | */ 57 | public abstract Type getGenericPlatformType(); 58 | 59 | /** 60 | * Casts an object to the type represented by this type. 61 | * 62 | *

This method does not check the value against this type's constraints. 63 | * For parameterized types, this method may inspect the object and thus not run in constant time. 64 | * 65 | * @param value The value. 66 | * @return The value, casted to the platform type. 67 | * @throws ClassCastException If the value is not of the platform type. 68 | */ 69 | public abstract T cast(@Nonnull Object value); 70 | 71 | /** 72 | * Determines if the data type represented by this {@code SerializableType} 73 | * object is either the same as, or is a more general description of, the data 74 | * type represented by the specified {@code type} parameter. 75 | * 76 | *

Specifically, this checks whether every value assignable to {@code type} 77 | * can also be assigned to this {@code SerializableType}. 78 | *

forall x, type.accepts(x) => this.accepts(x)
79 | * 80 | * @param type the type to be checked 81 | * @return {@code true} if properties of {@code this} type are assignable from values of {@code type}. 82 | */ 83 | public final boolean isAssignableFrom(SerializableType type) { 84 | if (this.getClass() != type.getClass()) { 85 | return false; 86 | } 87 | 88 | @SuppressWarnings("unchecked") SerializableType that = (SerializableType) type; 89 | return this.checker.comprehends(this, that); 90 | } 91 | 92 | /** 93 | * Returns whether this type's constraints accept the given value. 94 | * 95 | * @see #test(Object) 96 | */ 97 | public final boolean accepts(T serializedValue) { 98 | return this.test(serializedValue).hasPassed(); 99 | } 100 | 101 | /** 102 | * Tests the given value against this type's constraints. 103 | * 104 | * @see #accepts(Object) 105 | * @see TypeCheckResult 106 | */ 107 | public final TypeCheckResult test(T serializedValue) { 108 | return this.checker.test(this, this.cast(Objects.requireNonNull(serializedValue))); 109 | } 110 | 111 | /** 112 | * Serializes this type to a persistent format using the given {@link TypeSerializer}. 113 | * 114 | * @param serializer The type serializer to use. 115 | * @param target The place to which this type is serialized. 116 | * @param The type to serialize to. 117 | * @see TypeSerializer 118 | */ 119 | public abstract void serialize(TypeSerializer serializer, S target); 120 | 121 | /** 122 | * Serializes a config primitive to a serialized form. The value given must 123 | * be compatible with the platform type as given by {@link #cast(Object)} and additionally 124 | * satisfy this type's particular constraints. 125 | * 126 | * @param value The value to serialize. 127 | * @param serializer A ValueSerializer defining the serialized form. 128 | * @param The type of the serialized form. 129 | * @return The serialized form of value. 130 | */ 131 | public abstract S serializeValue(T value, ValueSerializer serializer); 132 | 133 | /** 134 | * Deserializes a config primitive from a serialized form. 135 | * 136 | * @param elem The serialized form of the value. 137 | * @param serializer A ValueSerializer defining the serialized form. 138 | * @param The type of the serialized form. 139 | * @return The deserialized value. 140 | * @throws ValueDeserializationException If a value cannot be deserialized. 141 | */ 142 | public abstract T deserializeValue(S elem, ValueSerializer serializer) throws ValueDeserializationException; 143 | 144 | @Override 145 | public abstract String toString(); 146 | 147 | /** 148 | * Two serialized types are equal if and only if they are of the same kind 149 | * and both have the same constraints. 150 | */ 151 | @Override 152 | public abstract boolean equals(Object o); 153 | 154 | @Override 155 | public abstract int hashCode(); 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/tree/ConfigQuery.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.tree; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.FiberQueryException; 12 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 13 | 14 | /** 15 | * A query that can be run against any config tree to try and get a node. 16 | * 17 | *

A {@code ConfigQuery} follows a path in the tree, represented by a list of strings. 18 | * It can notably be used to retrieve nodes from various config trees with a similar structure. 19 | * 20 | * @param the type of queried tree nodes 21 | */ 22 | public final class ConfigQuery { 23 | /** 24 | * Creates a {@code ConfigQuery} for a branch with a specific path. 25 | * 26 | *

Each part of the path must correspond to a single node name. 27 | * The first part matches a direct child node of the root supplied to 28 | * the {@link #search(ConfigTree)} and {@link #run(ConfigTree)} methods. 29 | * Each additional name matches a node such that the nth name 30 | * matches a node at depth n, starting from the supplied tree. 31 | * The last name should be the name of the branch to retrieve. 32 | * 33 | * @param first the first name in the config path 34 | * @param more additional node names forming the config path 35 | * @return a config query for branches of existing trees 36 | */ 37 | public static ConfigQuery branch(String first, String... more) { 38 | return new ConfigQuery<>(ConfigBranch.class, null, first, more); 39 | } 40 | 41 | /** 42 | * Creates a {@code ConfigQuery} for a property with a specific path and value type. 43 | * 44 | *

Each part of the path must correspond to a single node name. 45 | * The first part matches a direct child node of the root supplied to 46 | * the {@link #search(ConfigTree)} and {@link #run(ConfigTree)} methods. 47 | * Each additional name matches a node such that the nth name 48 | * matches a node at depth n, starting from the supplied tree. 49 | * The last name should be the name of the leaf to retrieve. 50 | * 51 | *

The returned query will only match a leaf with a {@linkplain ConfigLeaf#getConfigType() config type} 52 | * that is identical to the given {@code propertyType}. 53 | * 54 | * @param propertyType a class object representing the type of values held by queried properties 55 | * @param first the first name in the config path 56 | * @param more additional node names forming the config path 57 | * @return a config query for leaves of existing trees 58 | */ 59 | public static ConfigQuery> leaf(SerializableType propertyType, String first, String... more) { 60 | return new ConfigQuery<>(ConfigLeaf.class, propertyType, first, more); 61 | } 62 | 63 | private final List path; 64 | private final Class nodeType; 65 | @Nullable 66 | private final SerializableType valueType; 67 | 68 | private ConfigQuery(Class nodeType, @Nullable SerializableType valueType, String first, String[] path) { 69 | this.nodeType = nodeType; 70 | this.valueType = valueType; 71 | this.path = new ArrayList<>(); 72 | this.path.add(first); 73 | this.path.addAll(Arrays.asList(path)); 74 | } 75 | 76 | /** 77 | * Searches a config tree for a node satisfying this query. 78 | * If none is found, {@code Optional.empty()} is returned. 79 | * 80 | * @param cfg the config tree to search in 81 | * @return an {@code Optional} describing the queried node, 82 | * or {@code Optional.empty()}. 83 | * @see #run(ConfigTree) 84 | */ 85 | public Optional search(ConfigTree cfg) { 86 | try { 87 | return Optional.of(this.run(cfg)); 88 | } catch (FiberQueryException e) { 89 | return Optional.empty(); 90 | } 91 | } 92 | 93 | /** 94 | * Runs this query on a config tree. 95 | * 96 | *

If this query's parameters do not match the config's structure, 97 | * a {@link FiberQueryException} carrying error details is thrown. 98 | * The exception's information can be used for further handling of the erroring config. 99 | * 100 | * @param cfg the config tree to run the query on 101 | * @return the queried node, with the right path and type 102 | * @throws FiberQueryException if this query's parameters do not match the config's structure 103 | * @see FiberQueryException.MissingChild 104 | * @see FiberQueryException.WrongType 105 | * @see #search(ConfigTree) 106 | */ 107 | @Nonnull 108 | public T run(ConfigTree cfg) throws FiberQueryException { 109 | List path = this.path; 110 | ConfigTree branch = cfg; 111 | int lastIndex = path.size() - 1; 112 | 113 | for (int i = 0; i < lastIndex; i++) { 114 | branch = this.lookupChild(branch, path.get(i), ConfigBranch.class, null); 115 | } 116 | 117 | @SuppressWarnings("unchecked") T result = 118 | (T) this.lookupChild(branch, path.get(lastIndex), this.nodeType, this.valueType); 119 | return result; 120 | } 121 | 122 | private N lookupChild(ConfigTree tree, String name, Class nodeType, @Nullable SerializableType valueType) throws FiberQueryException { 123 | ConfigNode node = tree.lookup(name); 124 | 125 | if (nodeType.isInstance(node) && (valueType == null || valueType.equals(((ConfigLeaf) node).getConfigType()))) { 126 | return nodeType.cast(node); 127 | } else if (node != null) { 128 | throw new FiberQueryException.WrongType(tree, node, nodeType, valueType); 129 | } else { 130 | throw new FiberQueryException.MissingChild(name, tree); 131 | } 132 | } 133 | 134 | /** 135 | * Returns a string representation of this query. 136 | * 137 | *

The string representation consists of the expected node type, followed 138 | * by the expected value type, if any, followed by a representation of this 139 | * query's path where individual node names are joined by dots. 140 | * 141 | * @return a string representation of this query 142 | */ 143 | @Override 144 | public String toString() { 145 | StringBuilder sb = new StringBuilder().append(this.nodeType.getSimpleName()); 146 | 147 | if (this.valueType != null) { 148 | sb.append('<').append(this.valueType).append('>'); 149 | } 150 | 151 | return sb.append("@'").append(String.join(".", this.path)).append('\'').toString(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/annotation/Setting.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import javax.annotation.RegEx; 9 | 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigAttribute; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.tree.ConfigBranch; 12 | 13 | /** 14 | * Marks a field as a setting. This annotation is optional unless the class has been annotated with {@code @Settings(onlyAnnotated = true)} 15 | */ 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target(ElementType.FIELD) 18 | public @interface Setting { 19 | /** 20 | * Marks a setting as constant or final. 21 | * 22 | *

Constant settings can not have their value changed after being initialised. 23 | * 24 | * @return whether or not this setting is constant 25 | * @deprecated constants should now be stored as schema {@link ConfigAttribute}s, 26 | * not as values in an editable config tree. 27 | */ 28 | @Deprecated boolean constant() default false; 29 | 30 | /** 31 | * Sets the name that will be used for this setting. 32 | * 33 | *

Custom names have a higher authority than {@link SettingNamingConvention naming conventions}, and will not be affected by them. 34 | * If the name is empty, the name of the field being annotated will be used and the naming convention will be applied to that name. 35 | * 36 | * @return An empty string ({@code ""}) if no custom name was set, or the custom name if one was set. 37 | */ 38 | String name() default ""; 39 | 40 | /** 41 | * Sets the comment that will be used for this setting. 42 | * 43 | *

If empty, no comment will be set. 44 | * 45 | * @return An empty string ({@code ""}) if no comment was set, or the comment if one was set. 46 | */ 47 | String comment() default ""; 48 | 49 | /** 50 | * Marks that this field should be ignored during serialisation. 51 | * 52 | * @return Whether or not this field should be ignored 53 | */ 54 | boolean ignore() default false; 55 | 56 | /** 57 | * Indicates that this setting represents a group of settings, rather than a single value. 58 | * 59 | * @see ConfigBranch 60 | */ 61 | @Target(ElementType.FIELD) 62 | @Retention(RetentionPolicy.RUNTIME) 63 | @interface Group { 64 | /** 65 | * Sets the name that will be used for this node. 66 | * 67 | *

Custom names have a higher authority than {@link SettingNamingConvention naming conventions}, and will not be affected by them. 68 | * If the name is empty, the name of the field being annotated will be used and the naming convention will be applied to that name. 69 | * 70 | * @return An empty string ({@code ""}) if no custom name was set, or the custom name if one was set. 71 | */ 72 | String name() default ""; 73 | } 74 | 75 | @Target({}) 76 | @interface Constrain { 77 | /** 78 | * Indicates that this value is limited to a range of numerical values. 79 | * 80 | * @see BigRange 81 | */ 82 | @Target({ElementType.TYPE_USE}) 83 | @Retention(RetentionPolicy.RUNTIME) 84 | @interface Range { 85 | /** 86 | * The minimum value allowed (inclusive). 87 | * 88 | *

Settings being constrained using this annotation must be equal to or greater than this value. 89 | * 90 | * @return the minimum value 91 | */ 92 | double min() default Double.NEGATIVE_INFINITY; 93 | 94 | /** 95 | * The maximum value allowed (inclusive). 96 | * 97 | *

Settings being constrained using this annotation must be equal to or 98 | * less than this {@code value}. 99 | * 100 | * @return the maximum value 101 | */ 102 | double max() default Double.POSITIVE_INFINITY; 103 | 104 | /** 105 | * The minimum allowed distance between adjacent valid values. Must be positive. 106 | * 107 | *

Settings being constrained using this annotation must be a whole 108 | * multiple of the step size greater than the minimum value. 109 | * 110 | * @return the step size 111 | */ 112 | double step() default Double.MIN_VALUE; 113 | } 114 | 115 | /** 116 | * Indicates that this value is limited to a range of numerical values with unbounded precision. 117 | * 118 | * @see Range 119 | * @see java.math.BigDecimal 120 | */ 121 | @Target({ElementType.TYPE_USE}) 122 | @Retention(RetentionPolicy.RUNTIME) 123 | @interface BigRange { 124 | /** 125 | * The minimum value for this field (inclusive). If left empty, there will be no minimum. 126 | * 127 | * @return the minimum value 128 | */ 129 | String min() default ""; 130 | 131 | /** 132 | * The maximum value for this field (inclusive). If left empty, there will be no maximum. 133 | * 134 | * @return the maximum value 135 | */ 136 | String max() default ""; 137 | 138 | /** 139 | * The minimum allowed distance between adjacent valid values. If empty, 140 | * equivalent to a step size of zero, otherwise the value must represent 141 | * a positive real number. 142 | * 143 | *

Settings being constrained using this annotation must be a whole 144 | * multiple of the step size greater than the minimum value. 145 | * 146 | * @return the step size 147 | */ 148 | String step() default ""; 149 | } 150 | 151 | /** 152 | * Indicates that this value's length or size is limited to being equal to or larger than a number. 153 | * 154 | *

For example, a string annotated with {@code MinLength(5)} must be 5 or more characters long. {@code "ABCD"} would not be allowed, but {@code "ABCDE"} would be. 155 | * 156 | * @see MaxLength MaxLength 157 | */ 158 | @Target({ElementType.TYPE_USE}) 159 | @Retention(RetentionPolicy.RUNTIME) 160 | @interface MinLength { 161 | /** 162 | * Returns the minimum length allowed. 163 | * 164 | * @return the minimum length allowed 165 | * @see MinLength MinLength 166 | */ 167 | int value(); 168 | } 169 | 170 | /** 171 | * Indicates that this value's length or size is limited to being equal to or smaller than a number. 172 | * 173 | *

For example, a string annotated with {@code MaxLength(5)} must be 5 or less characters long. {@code "ABCDE"} would be allowed, but {@code "ABCDEF"} would not be. 174 | * 175 | * @see MinLength MinLength 176 | */ 177 | @Target({ElementType.TYPE_USE}) 178 | @Retention(RetentionPolicy.RUNTIME) 179 | @interface MaxLength { 180 | int value(); 181 | } 182 | 183 | /** 184 | * Indicates that this value's string representation must match a certain regex. 185 | */ 186 | @Target({ElementType.TYPE_USE}) 187 | @Retention(RetentionPolicy.RUNTIME) 188 | @interface Regex { 189 | /** 190 | * The regex this value must match. 191 | * 192 | * @return the regex 193 | * @see Regex Regex 194 | */ 195 | @RegEx String value(); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/main/java/io/github/fablabsmc/fablabs/api/fiber/v1/schema/type/derived/ConfigType.java: -------------------------------------------------------------------------------- 1 | package io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.derived; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.lang.reflect.AnnotatedElement; 5 | import java.util.Objects; 6 | import java.util.StringJoiner; 7 | import java.util.function.Function; 8 | 9 | import io.github.fablabsmc.fablabs.api.fiber.v1.annotation.processor.ConstraintAnnotationProcessor; 10 | import io.github.fablabsmc.fablabs.api.fiber.v1.exception.FiberConversionException; 11 | import io.github.fablabsmc.fablabs.api.fiber.v1.schema.type.SerializableType; 12 | 13 | /** 14 | * A {@code ConfigType} contains serializable information describing 15 | * a data type as well as the information necessary to convert a serialized 16 | * value of that type to a friendlier runtime type. 17 | * 18 | * @param the runtime type used by the client application 19 | * @param the serialized type used by config trees and serializers 20 | * @param the representation of the serialized type 21 | * @see SerializableType 22 | * @see BooleanConfigType 23 | * @see EnumConfigType 24 | * @see ListConfigType 25 | * @see MapConfigType 26 | * @see NumberConfigType 27 | * @see RecordConfigType 28 | * @see StringConfigType 29 | */ 30 | public abstract class ConfigType> { 31 | private final T serializedType; 32 | private final Class runtimeType; 33 | protected final Function deserializer; 34 | protected final Function serializer; 35 | 36 | ConfigType(T serializedType, Class runtimeType, Function deserializer, Function serializer) { 37 | this.runtimeType = runtimeType; 38 | this.serializedType = serializedType; 39 | this.deserializer = deserializer; 40 | this.serializer = serializer; 41 | } 42 | 43 | /** 44 | * Derive a {@code ConfigType} from this type object. 45 | * 46 | *

The new {@code ConfigType} will have the same serialized type (with the same constraints), 47 | * but a different runtime type. Values will be converted between the two 48 | * types using composed functions: {@code toSerializedType(x) = this.toSerializedType(partialSerializer(x))} 49 | * and {@code toRuntimeType(y) = partialDeserializer(this.toRuntimeType(y))}. 50 | * 51 | * @param runtimeType a class object representing the runtime type of the new {@code ConfigType} 52 | * @param partialDeserializer a partial deserialization function 53 | * @param partialSerializer a partial serialization function 54 | * @param the runtime type of the new {@code ConfigType} 55 | * @return a derived {@code ConfigType} with the given {@code runtimeType} 56 | */ 57 | public abstract ConfigType derive(Class runtimeType, Function partialDeserializer, Function partialSerializer); 58 | 59 | /** 60 | * Replace the current serialized type used for specification with the given serialized type. 61 | * The given serialized type must be the same or more constrained than the current 62 | * serialized type. 63 | * 64 | * @param newSpec The new type specification. 65 | * @return A ConfigType with the new type specification. 66 | * @see SerializableType#isAssignableFrom(SerializableType) 67 | */ 68 | public abstract ConfigType withType(T newSpec); 69 | 70 | /** 71 | * Converts a runtime value from a client application to this {@code ConfigType}'s 72 | * serialized type. 73 | * 74 | * @param runtimeValue the value to convert to serializable form 75 | * @return a serializable equivalent of the runtime value 76 | * @throws FiberConversionException if the value does not fit this converter's constraint 77 | * @see SerializableType#accepts(Object) 78 | * @see #toPlatformType(Object) 79 | */ 80 | public S toSerializedType(R runtimeValue) throws FiberConversionException { 81 | S s = this.toPlatformType(runtimeValue); 82 | 83 | if (!this.serializedType.accepts(s)) { 84 | throw new FiberConversionException("Serialized form " + s + "(" + this.serializedType.getErasedPlatformType().getSimpleName() + ") of runtime value " + runtimeValue + "(" + this.runtimeType.getSimpleName() + ") does not satisfy constraints for type " + this.serializedType); 85 | } 86 | 87 | return s; 88 | } 89 | 90 | /** 91 | * Converts directly a runtime value from a client application to an equivalent value in the serializable 92 | * {@linkplain SerializableType#getGenericPlatformType() platform type}. This method gives no guarantees regarding 93 | * the conformance of the returned value to the serialized type's constraints. 94 | * 95 | * @param runtimeValue the value to convert to serializable form 96 | * @return an unchecked serializable equivalent of the runtime value 97 | * @see SerializableType#getGenericPlatformType() 98 | * @see #toSerializedType(Object) 99 | */ 100 | public S toPlatformType(R runtimeValue) { 101 | return this.serializer.apply(Objects.requireNonNull(runtimeValue)); 102 | } 103 | 104 | /** 105 | * Converts a serialized value to this {@code ConfigType}'s runtime type. 106 | * 107 | * @param serializedValue the value to convert to runtime form 108 | * @return a runtime equivalent of the serialized value 109 | * @throws FiberConversionException if the serialized value does not fit this converter's constraints 110 | */ 111 | public R toRuntimeType(S serializedValue) throws FiberConversionException { 112 | if (!this.serializedType.accepts(serializedValue)) { 113 | throw new FiberConversionException("Invalid serialized value " + serializedValue); 114 | } 115 | 116 | return Objects.requireNonNull(this.deserializer.apply(serializedValue)); 117 | } 118 | 119 | /** 120 | * The runtime type of values. 121 | */ 122 | public Class getRuntimeType() { 123 | return this.runtimeType; 124 | } 125 | 126 | /** 127 | * The underlying serialized type of values. 128 | */ 129 | public T getSerializedType() { 130 | return this.serializedType; 131 | } 132 | 133 | @Override 134 | public String toString() { 135 | return new StringJoiner(", ", this.getClass().getSimpleName() + "[", "]") 136 | .add("serializedType=" + this.serializedType) 137 | .add("runtimeType=" + this.runtimeType) 138 | .toString(); 139 | } 140 | 141 | /** 142 | * Applies the constraints defined by the given constraint annotation. 143 | * 144 | * @param processor The processor for constraints of this type. 145 | * @param annotation The annotation from which to extract constraints. 146 | * @param annotated The annotated element. For example, a field in a POJO. 147 | * @return a ConfigType representing this type, but constrained by the given constraint annotation. 148 | */ 149 | public abstract ConfigType constrain(ConstraintAnnotationProcessor processor, Annotation annotation, AnnotatedElement annotated); 150 | 151 | void checkTypeNarrowing(T newSpec) { 152 | if (!this.serializedType.isAssignableFrom(newSpec)) { 153 | throw new IllegalStateException("Cannot widen the constraints on a TypeConverter"); 154 | } 155 | } 156 | } 157 | --------------------------------------------------------------------------------