diff --git a/src/main/java/app/quickcase/sdk/spring/condition/validate/ConditionValidator.java b/src/main/java/app/quickcase/sdk/spring/condition/validate/ConditionValidator.java new file mode 100644 index 0000000..88a4957 --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/condition/validate/ConditionValidator.java @@ -0,0 +1,9 @@ +package app.quickcase.sdk.spring.condition.validate; + +import java.util.Set; + +import app.quickcase.sdk.spring.condition.Condition; + +public interface ConditionValidator { + Set validate(Condition condition); +} diff --git a/src/main/java/app/quickcase/sdk/spring/condition/validate/SemanticConditionValidator.java b/src/main/java/app/quickcase/sdk/spring/condition/validate/SemanticConditionValidator.java new file mode 100644 index 0000000..e00e079 --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/condition/validate/SemanticConditionValidator.java @@ -0,0 +1,46 @@ +package app.quickcase.sdk.spring.condition.validate; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import app.quickcase.sdk.spring.condition.Condition; +import app.quickcase.sdk.spring.condition.Criteria; +import app.quickcase.sdk.spring.definition.DefinitionExtractor; +import app.quickcase.sdk.spring.path.FieldPath; + +public class SemanticConditionValidator implements ConditionValidator { + + private final DefinitionExtractor fieldExtractor; + + public SemanticConditionValidator(DefinitionExtractor fieldExtractor) { + this.fieldExtractor = fieldExtractor; + } + + @Override + public Set validate(Condition condition) { + var allPaths = Arrays.stream(condition.disjunctions()) + .flatMap(Arrays::stream) + .map(Criteria::getPath) + .collect(Collectors.toSet()); + + return allPaths.stream() + .flatMap(this::validate) + .collect(Collectors.toSet()); + } + + private Stream validate(String path) { + try { + var fieldPath = FieldPath.of(path); + + if (fieldExtractor.extractField(fieldPath).isEmpty()) { + return Stream.of("Path `" + path + "` does not exist"); + } + + return Stream.empty(); + } catch (IllegalArgumentException e) { + return Stream.of("Path `" + path + "` is not valid"); + } + } +} diff --git a/src/main/java/app/quickcase/sdk/spring/definition/DefinitionExtractor.java b/src/main/java/app/quickcase/sdk/spring/definition/DefinitionExtractor.java new file mode 100644 index 0000000..7fd6933 --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/definition/DefinitionExtractor.java @@ -0,0 +1,131 @@ +package app.quickcase.sdk.spring.definition; + +import java.util.ArrayDeque; +import java.util.Map; +import java.util.Optional; + +import app.quickcase.sdk.spring.definition.model.DataField; +import app.quickcase.sdk.spring.definition.model.Field; +import app.quickcase.sdk.spring.definition.model.MetadataField; +import app.quickcase.sdk.spring.definition.model.RecordType; +import app.quickcase.sdk.spring.path.DataFieldPath; +import app.quickcase.sdk.spring.path.FieldPath; +import app.quickcase.sdk.spring.path.MetadataFieldPath; + +public class DefinitionExtractor { + + private final RecordType type; + + public DefinitionExtractor(RecordType type) { + this.type = type; + } + + public Optional extractField(FieldPath path) { + return switch (path) { + case MetadataFieldPath metadataFieldPath -> Optional.of(extractField(metadataFieldPath)); + case DataFieldPath dataFieldPath -> extractField(dataFieldPath); + default -> throw new IllegalArgumentException("Unsupported field path: " + path); + }; + } + + public MetadataField extractField(MetadataFieldPath path) { + return switch (path.getMetadata()) { + case WORKSPACE -> MetadataField.builder() + .id(path.toString()) + .name("Workspace") + .label("Workspace") + .build(); + case TYPE -> MetadataField.builder() + .id(path.toString()) + .name("Type") + .label("Record type") + .build(); + case ID -> MetadataField.builder() + .id(path.toString()) + .name("ID") + .label("Record ID") + .build(); + case TITLE -> MetadataField.builder() + .id(path.toString()) + .name("Title") + .label("Record title") + .build(); + case STATE -> MetadataField.builder() + .id(path.toString()) + .name("State") + .label("Record state") + .build(); + case CREATED_AT -> MetadataField.builder() + .id(path.toString()) + .name("Created at") + .label("Date and time of creation") + .build(); + case LAST_MODIFIED_AT -> MetadataField.builder() + .id(path.toString()) + .name("Last modified at") + .label("Date and time of last modification") + .build(); + case CLASSIFICATION -> MetadataField.builder() + .id(path.toString()) + .name("Classification") + .label("Default record classification") + .build(); + }; + } + + public Optional extractField(DataFieldPath path) { + return extractDataField(type.schema().fields(), new ArrayDeque<>(path.elements())); + } + + private Optional extractDataField(Map fields, ArrayDeque pathElements) { + var element = pathElements.removeFirst(); + var field = fields.get(element.getIdentifier()); + + if (field == null && element.isCollectionItem()) { + return extractDataFieldCollectionItem(fields, element.getCollectionIdentifier(), pathElements); + } + + if (field == null) { + return Optional.empty(); + } + + if (!pathElements.isEmpty()) { + if (field.members() == null) { + return Optional.empty(); + } + + return extractDataField(field.members(), pathElements); + } + + return Optional.of(field); + } + + private Optional extractDataFieldCollectionItem(Map fields, String collectionId, ArrayDeque elements) { + var field = fields.get(collectionId); + + if (field == null) { + return Optional.empty(); + } + + if (!elements.isEmpty()) { + var itemFieldBuilder = DataField.builder() + .id(field.id() + "[].value") + .name(field.name()) + .label(field.label()) + .type(field.content().type()) + .acl(field.acl()) + .classification(field.classification()); + if (field.content().members() != null) { + itemFieldBuilder.members(field.content().members()); + } + + if (field.content().options() != null) { + itemFieldBuilder.options(field.content().options()); + } + + return extractDataField(Map.of("value", itemFieldBuilder.build()), elements); + } + + return Optional.of(field); + } +} diff --git a/src/main/java/app/quickcase/sdk/spring/definition/model/DataField.java b/src/main/java/app/quickcase/sdk/spring/definition/model/DataField.java new file mode 100644 index 0000000..1c10fbd --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/definition/model/DataField.java @@ -0,0 +1,45 @@ +package app.quickcase.sdk.spring.definition.model; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import lombok.Builder; +import lombok.NonNull; +import lombok.Singular; + +@Builder +public record DataField( + @NonNull String id, + @NonNull String name, + String label, + String hint, + @NonNull String type, + @Singular List