diff --git a/artifacts/build.gradle.kts b/artifacts/build.gradle.kts index 3f58705..7d559dd 100644 --- a/artifacts/build.gradle.kts +++ b/artifacts/build.gradle.kts @@ -1,3 +1,6 @@ +import org.eclipse.dsp.generation.SchemaTableGeneratorPlugin +import org.eclipse.dsp.generation.SchemaTableGeneratorPluginExtension + /* * Copyright (c) 2024 Metaform Systems, Inc. * @@ -17,10 +20,17 @@ plugins { checkstyle } +apply(); + repositories { mavenCentral() } +configure { + schemaPrefix = "https://w3id.org/dspace/2024/1/" + schemaFileSuffix = "-schema.json" +} + dependencies { implementation("com.networknt:json-schema-validator:1.5.2") { exclude("com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml") diff --git a/artifacts/buildSrc/README.md b/artifacts/buildSrc/README.md new file mode 100644 index 0000000..e6265c8 --- /dev/null +++ b/artifacts/buildSrc/README.md @@ -0,0 +1,91 @@ +# Introduction + +This directory contains the Schema Table Generator plugin. The plugin generates readable type information in HTML for Json +schema definitions found in the project source set. + +For example: + +```html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MessageOffer
Required properties
@idstring
@typestringValue must be Offer
Optional properties
obligationarray
permissionarray
profileany
+``` + +For each type, a generated HTML table is created and output to `/generated/tables`. File names are the +typename in lowercase with an `.html` suffix. These files may then be imported into the ReSpec-based documentation using +the `aside` element in the appropriate Markdown file: + +```html + + +``` + +## Implementation notes + +The Json Schema Object Model parser does not yet support all Json Schema features such `anyOf` or `oneOf`. + +# Build Setup + +The plugin can be applied and configured as follows: + +```kotlin +apply(); + +configure { + schemaPrefix = "https://w3id.org/dspace/2024/1/" + schemaFileSuffix = "-schema.json" +} +``` + +The `schemPrefix` property is used to specify the base URL for resolving schema references. This base URL will be mapped +relative to where the schema files reside on the local filesystem. The `schemaFileSuffix` propery is used to filter +schema files to include. + +# Running + +The generation process can be run by specifying the `generateTablesFromSchemas` task: + +``` +./gradlew generateTablesFromSchemas +``` + +To debug the generation process, use: + +``` +./gradlew -Dorg.gradle.debug=true --no-daemon generateTablesFromSchemas +``` + diff --git a/artifacts/buildSrc/build.gradle.kts b/artifacts/buildSrc/build.gradle.kts new file mode 100644 index 0000000..2da7de1 --- /dev/null +++ b/artifacts/buildSrc/build.gradle.kts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +plugins { + `java-library` + checkstyle +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.networknt:json-schema-validator:1.5.2") { + exclude("com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml") + } + testImplementation("org.assertj:assertj-core:3.26.3") +} + +testing { + suites { + val test by getting(JvmTestSuite::class) { + useJUnitJupiter("5.8.1") + } + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/SchemaTableGeneratorPlugin.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/SchemaTableGeneratorPlugin.java new file mode 100644 index 0000000..80ed744 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/SchemaTableGeneratorPlugin.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation; + + +import org.eclipse.dsp.generation.jsom.JsomParser; +import org.eclipse.dsp.generation.transformer.HtmlTableTransformer; +import org.eclipse.dsp.generation.transformer.SchemaTypeTransformer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSet; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import static java.util.Objects.requireNonNull; + +/** + * Generates a table of schema properties to be included in the specification text. + */ +public class SchemaTableGeneratorPlugin implements Plugin { + private static final String TASK_NAME = "generateTablesFromSchemas"; + private static final String CONFIG_NAME = "schemaTableGenerator"; + private static final String GENERATED = "generated"; + private static final String TABLES = "tables"; + + private final SchemaTypeTransformer htmlTransformer = new HtmlTableTransformer(); + + @Override + public void apply(@NotNull Project project) { + var extension = project.getExtensions().create(CONFIG_NAME, SchemaTableGeneratorPluginExtension.class); + + project.task(TASK_NAME).doLast(task -> { + var tablesDir = task.getProject().getLayout().getBuildDirectory().dir(GENERATED).get().dir(TABLES).getAsFile(); + //noinspection ResultOfMethodCallIgnored + tablesDir.mkdirs(); + var sourceSet = requireNonNull(project.getExtensions() + .findByType(JavaPluginExtension.class)).getSourceSets().getByName("main"); + + var prefix = extension.getSchemaPrefix(); + String resolvePath = getResolutionPath(sourceSet); + + // parse the schema object model + var parser = new JsomParser(prefix, resolvePath); + var stream = sourceSet.getResources().getFiles().stream() + .filter(f -> f.getName().endsWith(extension.getSchemaFileSuffix())); + var schemaModel = parser.parseFiles(stream); + + schemaModel.getSchemaTypes().stream() + .filter(type -> !type.isRootDefinition() && !type.isJsonBaseType()) // do not process built-in Json types and root schema types + .forEach(type -> { + var content = htmlTransformer.transform(type); + var destination = new File(tablesDir, type.getName().toLowerCase() + ".html"); + try (var writer = new FileWriter(destination)) { + writer.write(content); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + task.getLogger().info("Completed generation"); + }); + + } + + private String getResolutionPath(SourceSet sourceSet) { + var files = sourceSet.getResources().getSourceDirectories().getFiles(); + if (files.isEmpty()) { + throw new IllegalStateException("No schema resource directories found"); + } + var path = files.iterator().next(); + return path.getAbsolutePath().endsWith("/") ? path.getAbsolutePath() : path.getAbsolutePath() + "/"; + } + + +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/SchemaTableGeneratorPluginExtension.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/SchemaTableGeneratorPluginExtension.java new file mode 100644 index 0000000..a1f6942 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/SchemaTableGeneratorPluginExtension.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation; + +/** + * Defines the plugin configuration. + */ +public class SchemaTableGeneratorPluginExtension { + private String schemaPrefix; + private String schemaFileSuffix = "-schema.json"; + + public SchemaTableGeneratorPluginExtension() { + } + + public String getSchemaPrefix() { + return schemaPrefix; + } + + public void setSchemaPrefix(String schemaPrefix) { + this.schemaPrefix = schemaPrefix; + } + + public String getSchemaFileSuffix() { + return schemaFileSuffix; + } + + public void setSchemaFileSuffix(String schemaFileSuffix) { + this.schemaFileSuffix = schemaFileSuffix; + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/ElementDefinition.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/ElementDefinition.java new file mode 100644 index 0000000..68f44d7 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/ElementDefinition.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.jetbrains.annotations.NotNull; + +import java.util.Set; +import java.util.TreeSet; + +/** + * Models an element such as {@code contains} object or an {@code items} object. + */ +public class ElementDefinition implements Comparable { + + public enum Type { + REFERENCE, CONSTANT + } + + private final Type type; + private final String value; + private final Set resolvedTypes = new TreeSet<>(); + + public ElementDefinition(Type type, String value) { + this.type = type; + this.value = value; + } + + public Type getType() { + return type; + } + + public String getValue() { + return value; + } + + public Set getResolvedTypes() { + return resolvedTypes; + } + + public void resolvedType(SchemaType resolvedType) { + this.resolvedTypes.add(resolvedType); + } + + @Override + public int compareTo(@NotNull ElementDefinition o) { + return value.compareTo(o.value); + } + +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsomParser.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsomParser.java new file mode 100644 index 0000000..861e76a --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsomParser.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.stream.Stream.concat; +import static org.eclipse.dsp.generation.jsom.ElementDefinition.Type.CONSTANT; +import static org.eclipse.dsp.generation.jsom.ElementDefinition.Type.REFERENCE; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.ALL_OF; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.CONST; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.CONTAINS; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.DEFINITIONS; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.ITEMS; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.PROPERTIES; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.REF; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.REQUIRED; +import static org.eclipse.dsp.generation.jsom.JsonSchemaKeywords.TYPE; +import static org.eclipse.dsp.generation.jsom.JsonTypes.ANY; +import static org.eclipse.dsp.generation.jsom.JsonTypes.ARRAY; + +/** + * Parses JSON Schemas into a JSON Schema Object Model (JSOM). + */ +public class JsomParser { + private final ObjectMapper mapper; + private final String prefix; + private final String resolutionPath; + + public record SchemaInstance(String schemaPath, Map schema) { + } + + /** + * Ctor + * + * @param prefix the schema prefix used when referencing type definitions + * @param resolutionPath the local path that maps to the schema prefix + */ + public JsomParser(String prefix, String resolutionPath) { + this(prefix, resolutionPath, new ObjectMapper()); + } + + /** + * Ctor + * + * @param prefix the schema prefix used when referencing type definitions + * @param resolutionPath the local path that maps to the schema prefix + * @param mapper an object mapper used for schema deserialization + */ + public JsomParser(String prefix, String resolutionPath, ObjectMapper mapper) { + this.prefix = prefix; + this.resolutionPath = resolutionPath; + this.mapper = mapper; + } + + /** + * Parses a set of JSON schemas from the file locations and returns an object model representing the type system. + */ + @SuppressWarnings("unchecked") + public SchemaModel parseFiles(Stream files) { + var schemaContext = new SchemaModelContext(); + files.flatMap(schemaFile -> { + try { + var root = mapper.readValue(schemaFile, Map.class); + return parseTypes(schemaFile.getAbsolutePath(), (Map) root); + } catch (IOException e) { + throw new RuntimeException(e); + } + }).forEach(schemaContext::addType); + schemaContext.resolve(); + return schemaContext; + } + + /** + * Parses a set of JSON schemas instances and returns an object model representing the type system. + */ + public SchemaModel parseInstances(Stream instances) { + var schemaContext = new SchemaModelContext(); + instances.flatMap(instance -> parseTypes(instance.schemaPath(), instance.schema())).forEach(schemaContext::addType); + schemaContext.resolve(); + return schemaContext; + } + + @SuppressWarnings("unchecked") + private Stream parseTypes(String schemaPath, Map parsedSchema) { + var definitions = (Map) parsedSchema.getOrDefault(DEFINITIONS, Map.of()); + var definitionsStream = definitions.entrySet().stream() + .map(entry -> parseTypeDefinition(entry.getKey(), schemaPath, (Map) entry.getValue())); + + var rootAllOf = parsedSchema.get(ALL_OF); + if (rootAllOf != null) { + var rootType = parseRootType(schemaPath, parsedSchema); + return concat(Stream.of(rootType), definitionsStream); + } + return definitionsStream; + + } + + /** + * Parses a type definition at the schema root, i.e. its type name is the schema URI. + */ + private @NotNull SchemaType parseRootType(String schemaPath, Map root) { + var typeName = prefix + schemaPath.substring(resolutionPath.length()); + var baseType = root.getOrDefault(TYPE, ANY.getName()).toString(); + var rootType = new SchemaType(typeName, baseType, true, typeName); + parseAttributes(root, rootType); + return rootType; + } + + /** + * Parses a type definition at in the {@code definitions} schema property. + */ + private @NotNull SchemaType parseTypeDefinition(String type, String schemaPath, Map definition) { + var baseType = definition.getOrDefault(TYPE, ANY.getName()).toString(); + var context = prefix + schemaPath.substring(resolutionPath.length()); + var schemaType = new SchemaType(type, baseType, context); + parseAttributes(definition, schemaType); + return schemaType; + } + + @SuppressWarnings("unchecked") + private void parseAttributes(Map definition, SchemaType schemaType) { + parseRequired(definition, schemaType); + + // parse properties + var properties = (Map) definition.get(PROPERTIES); + if (properties != null) { + var schemaProperties = properties.entrySet().stream() + .map(e -> parseProperty(e.getKey(), (Map) e.getValue())) + .filter(Objects::nonNull) + .toList(); + schemaType.properties(schemaProperties); + } + + // parse allOf properties + parseAllOf(definition, schemaType); + + // parse contains + var contains = (Map) definition.get(CONTAINS); + if (contains != null) { + schemaType.contains(parseElementDefinition(contains)); + } + } + + private List parseElementDefinition(Map container) { + var constantValue = (String) container.get(CONST); + if (constantValue != null) { + return List.of(new ElementDefinition(CONSTANT, constantValue)); + } else { + var refValue = (String) container.get(REF); + if (refValue != null) { + return List.of(new ElementDefinition(REFERENCE, refValue)); + } + } + return emptyList(); + } + + @SuppressWarnings("unchecked") + private void parseAllOf(Map definition, SchemaType schemaType) { + var allOfDefinition = (List>) definition.getOrDefault(ALL_OF, emptyList()); + var allOfProperties = allOfDefinition.stream() + .map(e -> (Map) e.get(PROPERTIES)) + .filter(Objects::nonNull) + .flatMap(e -> e.entrySet().stream()) + .map(e -> parseProperty(e.getKey(), (Map) e.getValue())) + .toList(); + schemaType.properties(allOfProperties); + + // parse allOf references + var allOf = allOfDefinition + .stream() + .map(e -> e.get(REF)) + .filter(Objects::nonNull) + .map(Object::toString) + .toList(); + schemaType.allOf(allOf); + } + + @SuppressWarnings("unchecked") + private void parseRequired(Map definition, SchemaType schemaType) { + // parse required + var required = (List) definition.get(REQUIRED); + if (required != null) { + var requiredFields = required.stream().map(SchemaPropertyReference::new).toList(); + schemaType.required(requiredFields); + } + } + + @SuppressWarnings("unchecked") + private SchemaProperty parseProperty(String name, Map value) { + var type = value.get(TYPE); + if (type == null) { + var ref = value.get(REF); + if (ref != null) { + return SchemaProperty.Builder.newInstance() + .name(name) + .types(Set.of((String) ref)) + .description("") + .build(); + } + return SchemaProperty.Builder.newInstance() + .name(name) + .types(Set.of(ANY.getName())) + .description("") + .build(); + } else if (ARRAY.getBaseType().equals(type)) { + var property = SchemaProperty.Builder.newInstance() + .name(name) + .types(Set.of((String) type)) + .description(""); + var items = value.get(ITEMS); + if (items instanceof Map) { + property.itemTypes(parseElementDefinition((Map) items)); + } + return property.build(); + } else { + var builder = SchemaProperty.Builder.newInstance() + .name(name) + .types(Set.of((String) type)) + .description(""); + var constantValue = value.get(CONST); + if (constantValue != null) { + builder.constantValue(constantValue.toString()); + } + return builder.build(); + } + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonSchemaKeywords.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonSchemaKeywords.java new file mode 100644 index 0000000..d54e4de --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonSchemaKeywords.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +/** + * Json Schema keywords. + */ +public interface JsonSchemaKeywords { + String REF = "$ref"; + String ALL_OF = "allOf"; + String ANY_OF = "anyOf"; + String CONST = "const"; + String CONTAINS = "contains"; + String DEFINITIONS = "definitions"; + String ITEMS = "items"; + String ONE_OF = "oneOf"; + String PROPERTIES = "properties"; + String REQUIRED = "required"; + String TYPE = "type"; +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonTypes.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonTypes.java new file mode 100644 index 0000000..f379a8c --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/JsonTypes.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +/** + * The Json Schema type system. + */ +public interface JsonTypes { + + SchemaType STRING = new SchemaType("string"); + + SchemaType NUMBER = new SchemaType("number"); + + SchemaType INTEGER = new SchemaType("integer"); + + SchemaType OBJECT = new SchemaType("object"); + + SchemaType ARRAY = new SchemaType("array"); + + SchemaType BOOLEAN = new SchemaType("boolean"); + + SchemaType NULL = new SchemaType("null"); + + SchemaType ANY = new SchemaType("any"); + +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModel.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModel.java new file mode 100644 index 0000000..99a8f1f --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModel.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; + +/** + * Represents a JSON Schema Object Model (JSOM). + */ +public interface SchemaModel { + + /** + * Resolves a reference to a schema type for a given context or null if not found. + */ + SchemaType resolveType(String reference, String typeContext); + + /** + * Returns all schema types defined in the model. + */ + @NotNull + Collection getSchemaTypes(); +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModelContext.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModelContext.java new file mode 100644 index 0000000..e373a20 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaModelContext.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static org.eclipse.dsp.generation.jsom.ElementDefinition.Type.REFERENCE; +import static org.eclipse.dsp.generation.jsom.JsonTypes.ANY; +import static org.eclipse.dsp.generation.jsom.JsonTypes.ARRAY; +import static org.eclipse.dsp.generation.jsom.JsonTypes.BOOLEAN; +import static org.eclipse.dsp.generation.jsom.JsonTypes.INTEGER; +import static org.eclipse.dsp.generation.jsom.JsonTypes.NULL; +import static org.eclipse.dsp.generation.jsom.JsonTypes.NUMBER; +import static org.eclipse.dsp.generation.jsom.JsonTypes.OBJECT; +import static org.eclipse.dsp.generation.jsom.JsonTypes.STRING; + +/** + * Contains a Json Schema object model and its types. + *

+ * Types must first be added to the context. After all types have been added, {@link #resolve()} will bind all type references in the model. + * For example, {@code $ref} entries will be linked to their actual parsed type instances. + */ +public class SchemaModelContext implements SchemaModel { + private static final String RELATIVE_REFERENCE = "#/definitions/"; + private static final String POINTER_REFERENCE = "#definitions/"; + private static final String POINTER_REFERENCE_SEPARATOR = "#/definitions/"; + + private final Map schemaTypes = new HashMap<>(); + private final Map> typesByContext = new HashMap<>(); + + public SchemaModelContext() { + // load built-in Json types + schemaTypes.put(STRING.getName(), STRING); + schemaTypes.put(NUMBER.getName(), NUMBER); + schemaTypes.put(INTEGER.getName(), INTEGER); + schemaTypes.put(OBJECT.getName(), OBJECT); + schemaTypes.put(ARRAY.getName(), ARRAY); + schemaTypes.put(BOOLEAN.getName(), BOOLEAN); + schemaTypes.put(NULL.getName(), NULL); + schemaTypes.put(ANY.getName(), ANY); + } + + @Override + public SchemaType resolveType(String reference, String typeContext) { + if (reference.startsWith(RELATIVE_REFERENCE)) { + // resolve references relative to the schema definition, e.g. in "definitions" + var typesForContext = typesByContext.get(typeContext); + if (typesForContext != null) { + return typesForContext.get(reference.substring(RELATIVE_REFERENCE.length())); + } + return null; + } else if (reference.contains(POINTER_REFERENCE)) { + // reference of type https://>#definitions/SomeType + var tokens = reference.split(POINTER_REFERENCE); + if (tokens.length != 2) { + throw new UnsupportedOperationException("Unsupported reference type: " + reference); + } + return resolveType(RELATIVE_REFERENCE + tokens[1], tokens[0]); + } else if (reference.contains(POINTER_REFERENCE_SEPARATOR)) { + var tokens = reference.split(POINTER_REFERENCE_SEPARATOR); + if (tokens.length != 2) { + throw new UnsupportedOperationException("Unsupported reference type: " + reference); + } + return resolveType(RELATIVE_REFERENCE + tokens[1], tokens[0]); + } else { + // resolve a reference to an external schema + return schemaTypes.get(reference); + } + } + + @NotNull + public Collection getSchemaTypes() { + return schemaTypes.values(); + } + + public void addType(SchemaType type) { + schemaTypes.put(type.getName(), type); + typesByContext.computeIfAbsent(type.getSchemaUri(), k -> new HashMap<>()).put(type.getName(), type); + } + + public void resolve() { + // resolve all schema type references and link them + var types = schemaTypes.values(); + types.forEach(type -> type.getAllOf().stream() + .map(ref -> resolveType(ref, type.getSchemaUri())) + .filter(Objects::nonNull) + .forEach(type::resolvedAllOfType)); + + // resolve all property type references and link them + types.forEach(type -> type.getProperties() + .forEach(property -> property.getTypes().stream() + .map((ref -> resolveType(ref, type.getSchemaUri()))) + .filter(Objects::nonNull) + .forEach(property::resolvedType))); + + // resolve required properties that do not reference properties explicitly defined on the type, e.g. + // required properties from types referenced in `allOf` + types.forEach(type -> type.getRequiredProperties() + .stream() + .filter(ref -> ref.getResolvedProperty() == null) + .forEach(ref -> { + // check in allOf + var resolved = type.getResolvedAllOf().stream() + .flatMap(t -> t.getProperties().stream() + .map(p -> p.getName().equals(ref.getName()) ? p : null) + .filter(Objects::nonNull)).findFirst().orElse(null); + + ref.resolvedProperty(resolved); + })); + + + // resolve contains references + types.forEach(type -> type.getContains().stream().filter(cd -> cd.getType() == REFERENCE) + .forEach(cd -> { + var resolved = resolveType(cd.getValue(), type.getSchemaUri()); + if (resolved != null) { + cd.resolvedType(resolved); + } + })); + + // resolve array item references + types.forEach(type -> { + type.getProperties().stream() + .flatMap(property -> property.getItemTypes().stream()) + .filter(item -> item.getType() == REFERENCE) + .forEach(item -> { + var resolved = resolveType(item.getValue(), type.getSchemaUri()); + if (resolved != null) { + item.resolvedType(resolved); + } + }); + }); + + // resolve required properties to the schema property definition + types.forEach(SchemaType::resolvePropertyReferences); + + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaProperty.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaProperty.java new file mode 100644 index 0000000..61c142c --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaProperty.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Set; +import java.util.TreeSet; + +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.joining; + +/** + * A property defined in a schema type. + */ +public class SchemaProperty implements Comparable { + private String name; + private String description = ""; + private String constantValue; + private Set types = new TreeSet<>(); + private final Set resolvedTypes = new TreeSet<>(); + private final Set itemTypes = new TreeSet<>(); + + private SchemaProperty() { + } + + public String getName() { + return name; + } + + public Set getTypes() { + return types; + } + + public String getConstantValue() { + return constantValue; + } + + public String getDescription() { + return description; + } + + public Set getResolvedTypes() { + return resolvedTypes; + } + + public void resolvedType(SchemaType resolvedType) { + this.resolvedTypes.add(resolvedType); + } + + public Set getItemTypes() { + return itemTypes; + } + + @Override + public int compareTo(@NotNull SchemaProperty o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + var b = new StringBuilder(name) + .append(": ") + .append(resolvedTypes.stream().map(SchemaType::getName).collect(joining(","))); + if (constantValue != null) { + b.append(" [").append(constantValue).append("]"); + } + return b.toString(); + } + + public static final class Builder { + private final SchemaProperty property; + + public static Builder newInstance() { + return new Builder(); + } + + public Builder name(String name) { + property.name = name; + return this; + } + + public Builder description(String description) { + property.description = description; + return this; + } + + public Builder types(Set types) { + property.types = types; + return this; + } + + public Builder itemTypes(Collection elementTypes) { + property.itemTypes.addAll(elementTypes); + return this; + } + + public Builder constantValue(String value) { + property.constantValue = value; + return this; + } + + public SchemaProperty build() { + requireNonNull(property.name); + requireNonNull(property.description); + if (property.types.isEmpty()) { + property.types.add("any"); + } + return property; + } + + private Builder() { + property = new SchemaProperty(); + } + + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaPropertyReference.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaPropertyReference.java new file mode 100644 index 0000000..792ddd8 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaPropertyReference.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +/** + * A reference to a schema property. + */ +public class SchemaPropertyReference implements Comparable { + private final String name; + private SchemaProperty resolvedProperty; + + public SchemaPropertyReference(String name) { + this.name = requireNonNull(name); + } + + public SchemaPropertyReference(String name, SchemaProperty resolvedProperty) { + this.name = name; + this.resolvedProperty = resolvedProperty; + } + + public String getName() { + return name; + } + + public SchemaProperty getResolvedProperty() { + return resolvedProperty; + } + + public void resolvedProperty(SchemaProperty property) { + this.resolvedProperty = property; + } + + @Override + public int compareTo(@NotNull SchemaPropertyReference o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return name; + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaType.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaType.java new file mode 100644 index 0000000..6b58d88 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/jsom/SchemaType.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toCollection; +import static java.util.stream.Stream.concat; + +/** + * A Json Schema type. + *

+ * {@link #getBaseType()} returns the JSON type, e.g. {@code object}, {@code array}, etc. of the type. {@link #getName()} returns + * the type name which is either the property name of its {@code definitions} entry in the schema, or if the type is defined + * in the root level of the schema, the latter's URI. + */ +public class SchemaType implements Comparable { + private static final String JSON_BASE_URI = "urn:json"; + + private final String name; + private final String baseType; + private final boolean rootDefinition; + private final String schemaUri; + private final Set allOf = new HashSet<>(); + private final Set resolvedAllOf = new HashSet<>(); + private final Set properties = new TreeSet<>(); + private final Set contains = new TreeSet<>(); + private final Map propertiesMap = new HashMap<>(); + private final Map requiredProperties = new HashMap<>(); + private final Map optionalProperties = new HashMap<>(); + + private boolean jsonBaseType; // denotes if this type represents a base Json type, e.g. string, object, array + + /** + * Ctor for base Json types. + */ + public SchemaType(String name) { + this(name, "any", JSON_BASE_URI); + this.jsonBaseType = true; + } + + /** + * Creates a type which a base type "any". + */ + public SchemaType(String name, String schemaUri) { + this(name, "any", schemaUri); + } + + public SchemaType(String name, String baseType, String schemaUri) { + this(name, baseType, false, schemaUri); + } + + public SchemaType(String name, String baseType, boolean rootDefinition, String schemaUri) { + this.name = name; + this.baseType = baseType; + this.rootDefinition = rootDefinition; + this.schemaUri = schemaUri; + } + + public String getName() { + return name; + } + + public boolean isJsonBaseType() { + return jsonBaseType; + } + + public String getBaseType() { + return baseType; + } + + public boolean isRootDefinition() { + return rootDefinition; + } + + public String getSchemaUri() { + return schemaUri; + } + + @NotNull + public Set getProperties() { + return properties; + } + + public Collection getRequiredProperties() { + return requiredProperties.values(); + } + + @NotNull + public Set getTransitiveRequiredProperties() { + return concat(requiredProperties.values().stream(), resolvedAllOf.stream() + .flatMap(type -> type.getTransitiveRequiredProperties().stream())) + .collect((toCollection(TreeSet::new))); + } + + @NotNull + public Set getTransitiveOptionalProperties() { + // a type by include multple other types (allOf) where a property is optional in one but mandatory in another - filter it + var required = getRequiredProperties().stream().map(SchemaPropertyReference::getName).collect(Collectors.toSet()); + return concat(optionalProperties.values().stream(), resolvedAllOf.stream() + .flatMap(type -> type.getTransitiveOptionalProperties().stream())) + .filter(prop -> !required.contains(prop.getName())) // filter required + .collect((toCollection(TreeSet::new))); + } + + public void properties(List schemaProperties) { + properties.addAll(schemaProperties); + schemaProperties.forEach(p -> propertiesMap.put(p.getName(), p)); + } + + public Set getContains() { + return contains; + } + + public void contains(Collection collection) { + contains.addAll(collection); + } + + @NotNull + public Set getAllOf() { + return allOf; + } + + public Set getResolvedAllOf() { + return resolvedAllOf; + } + + public void allOf(Collection allOf) { + this.allOf.addAll(allOf); + } + + public void resolvedAllOfType(SchemaType type) { + this.resolvedAllOf.add(type); + } + + public void required(Collection required) { + required.forEach(ref -> requiredProperties.put(ref.getName(), ref)); + } + + public void resolvePropertyReferences() { + // resolve required properties that have not yet been previously resolved, e.g. those directly defined on the type + requiredProperties.values() + .stream() + .filter(ref -> ref.getResolvedProperty() == null) + .forEach(ref -> ref.resolvedProperty(propertiesMap.get(ref.getName()))); + + // populate optional properties + properties.stream() + .filter(p -> !requiredProperties.containsKey(p.getName())) + .forEach(property -> optionalProperties.put(property.getName(), new SchemaPropertyReference(property.getName(), property))); + } + + @Override + public int compareTo(@NotNull SchemaType o) { + return name.compareTo(o.name); + } + + @Override + public String toString() { + return "SchemaType{" + + "name='" + name + '\'' + + ", baseType='" + baseType + '\'' + + '}'; + } +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformer.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformer.java new file mode 100644 index 0000000..16d1007 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformer.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.transformer; + +import org.eclipse.dsp.generation.jsom.ElementDefinition; +import org.eclipse.dsp.generation.jsom.SchemaProperty; +import org.eclipse.dsp.generation.jsom.SchemaPropertyReference; +import org.eclipse.dsp.generation.jsom.SchemaType; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Stream.concat; +import static org.eclipse.dsp.generation.jsom.ElementDefinition.Type.CONSTANT; +import static org.eclipse.dsp.generation.jsom.JsonTypes.OBJECT; + +/** + * Transforms a {@link SchemaType} into an HTML table representation. + */ +public class HtmlTableTransformer implements SchemaTypeTransformer { + + @Override + @NotNull + public String transform(SchemaType schemaType) { + var builder = new StringBuilder(CSS).append(""); + builder.append(format("", schemaType.getName())); + transformProperties("Required", schemaType.getTransitiveRequiredProperties(), builder); + transformProperties("Optional", schemaType.getTransitiveOptionalProperties(), builder); + return builder.append("
%s
").toString(); + } + + private void transformProperties(String title, Set references, StringBuilder builder) { + if (!references.isEmpty()) { + builder.append(format("%s properties", title)); + references.forEach(propertyReference -> transformProperty(propertyReference, builder)); + } + } + + private void transformProperty(SchemaPropertyReference propertyReference, StringBuilder builder) { + builder.append(""); + builder.append(format("%s", propertyReference.getName())); + var resolvedProperty = propertyReference.getResolvedProperty(); + if (resolvedProperty != null) { + String resolvedTypes = ""; + if (!resolvedProperty.getItemTypes().isEmpty()) { + resolvedTypes = getArrayTypeName(resolvedProperty); + } else { + resolvedTypes = resolvedProperty + .getResolvedTypes().stream().map(this::getTypeName).collect(joining(", ")); + } + builder.append(format("%s", resolvedTypes)); + if (resolvedProperty.getConstantValue() != null) { + builder.append(format("Value must be %s", resolvedProperty.getConstantValue())); + } else { + var constants = resolvedProperty.getResolvedTypes().stream() + .flatMap(t -> concat(Stream.of(t), t.getResolvedAllOf().stream())) // search the contains of the current type and any references 'allOf' types + .flatMap(t -> t.getContains().stream()) + .filter(cd -> cd.getType() == CONSTANT) + .map(ElementDefinition::getValue) + .collect(Collectors.joining("
")); + if (constants.isEmpty()) { + builder.append(format("%s", resolvedProperty.getDescription())); + } else { + builder.append(format("Must contain the following:
%s", constants)); + } + } + } + builder.append(""); + } + + private @NotNull String getArrayTypeName(SchemaProperty resolvedProperty) { + var itemTypes = resolvedProperty.getItemTypes().stream() + .flatMap(t -> t.getResolvedTypes().stream()).map(this::getTypeName) + .collect(joining(", ")); + if (itemTypes.isEmpty()) { + return "array"; + } + return "array[" + itemTypes + "]"; + } + + private String getTypeName(SchemaType schemaType) { + if (schemaType.isRootDefinition()) { + // root definition, check to see if it has an allOf, and if not, fallback to the Json base type + if (!schemaType.getResolvedAllOf().isEmpty()) { + // ue the allOf types and return the type name if it is a Json base object type; otherwise use the base type name + return schemaType.getResolvedAllOf().stream() + .map(t -> OBJECT.getName().equals(t.getBaseType()) ? t.getName() : t.getBaseType()) + .collect(joining(", ")); + } + return schemaType.getBaseType(); + } + return schemaType.getName(); + } + + private static final String CSS = """ + + """; +} diff --git a/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/SchemaTypeTransformer.java b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/SchemaTypeTransformer.java new file mode 100644 index 0000000..9fe2894 --- /dev/null +++ b/artifacts/buildSrc/src/main/java/org/eclipse/dsp/generation/transformer/SchemaTypeTransformer.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.transformer; + +import org.eclipse.dsp.generation.jsom.SchemaType; +import org.jetbrains.annotations.NotNull; + +/** + * Transforms a {@link SchemaType} to an output format. + */ +public interface SchemaTypeTransformer { + + @NotNull + OUTPUT transform(SchemaType schemaType); + +} diff --git a/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/JsomParserTest.java b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/JsomParserTest.java new file mode 100644 index 0000000..0698c6b --- /dev/null +++ b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/JsomParserTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.dsp.generation.jsom.TestSchema.TEST_SCHEMA; + +class JsomParserTest { + private static final String LOCAL = "/local/"; + private static final String SCHEMA_PREFIX = "http://foo.com/schema/"; + private ObjectMapper mapper; + private JsomParser parser; + + @Test + @SuppressWarnings("unchecked") + void verifyParse() throws JsonProcessingException { + var schema = (Map) mapper.readValue(TEST_SCHEMA, Map.class); + var schemaModel = parser.parseInstances(Stream.of(new JsomParser.SchemaInstance(LOCAL + "foo.json", schema))); + + assertThat(schemaModel.getSchemaTypes()).isNotEmpty(); + + var policyClass = schemaModel.resolveType("PolicyClass", LOCAL); + var agreement = schemaModel.resolveType("Agreement", LOCAL); + assertThat(agreement.getResolvedAllOf()).contains(policyClass); + assertThat(agreement.getProperties().size()).isEqualTo(5); // Agreement should have 5 properties + assertThat(agreement.getTransitiveRequiredProperties().size()).isEqualTo(5); + assertThat(agreement.getTransitiveOptionalProperties().size()).isEqualTo(4); + + + } + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + parser = new JsomParser(SCHEMA_PREFIX, LOCAL, mapper); + } +} \ No newline at end of file diff --git a/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaModelContextTest.java b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaModelContextTest.java new file mode 100644 index 0000000..d663805 --- /dev/null +++ b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaModelContextTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class SchemaModelContextTest { + private SchemaModelContext modelContext; + + @Test + public void verifyResolveRelativeReference() { + modelContext.addType(new SchemaType("Foo", "SchemaFile")); + assertThat(modelContext.resolveType("Foo", "SchemaFile")).isNotNull(); + assertThat(modelContext.resolveType("#/definitions/Foo", "SchemaFile")).isNotNull(); + } + + @Test + void verifyResolveReferenceToAnotherFile() { + modelContext.addType(new SchemaType("Foo", "SchemaFile")); + assertThat(modelContext.resolveType("SchemaFile#/definitions/Foo", "AnotherContext")).isNotNull(); + assertThat(modelContext.resolveType("SchemaFile#definitions/Foo", "AnotherContext")).isNotNull(); + } + + @Test + void verifyPropertyTypeResolution() { + var fooSchema = new SchemaType("Foo", "SchemaFile"); + var barSchema = new SchemaType("Bar", "SchemaFile"); + + var fooProperty = SchemaProperty.Builder.newInstance() + .name("foo") + .types(Set.of("#/definitions/Foo")) + .build(); + + var fooArrayProperty = SchemaProperty.Builder.newInstance() + .name("fooArray") + .types(Set.of("array")) + .itemTypes(Set.of(new ElementDefinition(ElementDefinition.Type.REFERENCE, "#/definitions/Foo"))) + .build(); + + var stringProperty = SchemaProperty.Builder.newInstance() + .name("stringProperty") + .types(Set.of("string")) + .build(); + + + barSchema.properties(List.of(fooProperty, fooArrayProperty, stringProperty)); + modelContext.addType(fooSchema); + modelContext.addType(barSchema); + modelContext.resolve(); + + assertThat(fooProperty.getResolvedTypes().size()).isEqualTo(1); + assertThat(fooProperty.getResolvedTypes()).contains(fooSchema); + + assertThat(fooArrayProperty.getItemTypes().iterator().next().getResolvedTypes()).contains(fooSchema); + assertThat(stringProperty.getResolvedTypes()).contains(JsonTypes.STRING); + } + + @Test + void verifyAllOfTypeResolution() { + var abstractFooSchema = new SchemaType("AbstractFoo", "SchemaFile"); + + var abstractProperty = SchemaProperty.Builder.newInstance() + .name("abstractProperty") + .types(Set.of("#/definitions/Foo")) + .build(); + + var abstractRequiredProperty = SchemaProperty.Builder.newInstance() + .name("abstractRequiredProperty") + .types(Set.of("string")) + .build(); + + abstractFooSchema.properties(List.of(abstractProperty, abstractRequiredProperty)); + + var fooSchema = new SchemaType("Foo", "SchemaFile"); + fooSchema.allOf(Set.of("#/definitions/AbstractFoo")); + fooSchema.required(Set.of(new SchemaPropertyReference("abstractRequiredProperty"))); + + modelContext.addType(abstractFooSchema); + modelContext.addType(fooSchema); + modelContext.resolve(); + + assertThat(fooSchema.getTransitiveOptionalProperties().size()).isEqualTo(1); + assertThat(fooSchema.getTransitiveOptionalProperties().iterator().next().getResolvedProperty()).isSameAs(abstractProperty); + + assertThat(fooSchema.getTransitiveRequiredProperties().size()).isEqualTo(1); + assertThat(fooSchema.getTransitiveRequiredProperties().iterator().next().getResolvedProperty()).isSameAs(abstractRequiredProperty); + } + + @Test + void verifyContainsTypeResolution() { + var fooSchema = new SchemaType("Foo", "SchemaFile"); + var barSchema = new SchemaType("Bar", "SchemaFile"); + + barSchema.contains(Set.of(new ElementDefinition(ElementDefinition.Type.REFERENCE, "#/definitions/Foo"))); + + modelContext.addType(fooSchema); + modelContext.addType(barSchema); + modelContext.resolve(); + + assertThat(barSchema.getContains().iterator().next().getResolvedTypes().iterator().next()).isEqualTo(fooSchema); + } + + @BeforeEach + void setUp() { + modelContext = new SchemaModelContext(); + } +} \ No newline at end of file diff --git a/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaTypeTest.java b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaTypeTest.java new file mode 100644 index 0000000..d50728d --- /dev/null +++ b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/SchemaTypeTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +class SchemaTypeTest { + + @Test + void verifyResolvePropertyReferences() { + var fooSchema = new SchemaType("Bar", "SchemaFile"); + + var requiredProperty = SchemaProperty.Builder.newInstance() + .name("requiredProperty") + .types(Set.of("string")) + .build(); + + var optionalProperty = SchemaProperty.Builder.newInstance() + .name("optionalProperty") + .types(Set.of("string")) + .build(); + + fooSchema.properties(List.of(requiredProperty, optionalProperty)); + fooSchema.required(Set.of(new SchemaPropertyReference("requiredProperty"))); + + fooSchema.resolvePropertyReferences(); + + assertThat(fooSchema.getTransitiveRequiredProperties().size()).isEqualTo(1); + assertThat(fooSchema.getTransitiveRequiredProperties().iterator().next().getResolvedProperty()).isEqualTo(requiredProperty); + + assertThat(fooSchema.getTransitiveOptionalProperties().size()).isEqualTo(1); + assertThat(fooSchema.getTransitiveOptionalProperties().iterator().next().getResolvedProperty()).isEqualTo(optionalProperty); + + } + + @Test + void verifyTransitiveProperties() { + var abstractFooSchema = new SchemaType("AbstractFoo", "SchemaFile"); + + var abstractProperty = SchemaProperty.Builder.newInstance() + .name("abstractProperty") + .types(Set.of("string")) + .build(); + + var abstractRequiredProperty = SchemaProperty.Builder.newInstance() + .name("abstractRequiredProperty") + .types(Set.of("string")) + .build(); + + abstractFooSchema.properties(List.of(abstractProperty, abstractRequiredProperty)); + + var fooSchema = new SchemaType("Foo", "SchemaFile"); + var abstractRequiredReference = new SchemaPropertyReference("abstractRequiredProperty"); + fooSchema.required(Set.of(abstractRequiredReference)); + fooSchema.resolvedAllOfType(abstractFooSchema); + + abstractFooSchema.resolvePropertyReferences(); + fooSchema.resolvePropertyReferences(); + + assertThat(fooSchema.getTransitiveRequiredProperties().size()).isEqualTo(1); + assertThat(fooSchema.getTransitiveRequiredProperties().iterator().next()).isSameAs(abstractRequiredReference); + + assertThat(fooSchema.getTransitiveOptionalProperties().size()).isEqualTo(1); + assertThat(fooSchema.getTransitiveOptionalProperties().iterator().next().getName()).isSameAs(abstractProperty.getName()); + + } +} \ No newline at end of file diff --git a/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/TestSchema.java b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/TestSchema.java new file mode 100644 index 0000000..0f4ec82 --- /dev/null +++ b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/jsom/TestSchema.java @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.jsom; + +/** + * Schema for testing. + */ +public interface TestSchema { + + String TEST_SCHEMA = """ + { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "title": "PolicySchema", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/Policy" + } + ], + "$id": "https://w3id.org/dspace/2024/1/negotiation/contract-schema.json", + "definitions": { + "Policy": { + "oneOf": [ + { + "$ref": "#/definitions/MessageOffer" + }, + { + "$ref": "#/definitions/Offer" + }, + { + "$ref": "#/definitions/Agreement" + } + ] + }, + "PolicyClass": { + "type": "object", + "properties": { + "@id": { + "type": "string" + }, + "profile": { + "oneOf": [ + { + "type": "array", + "items": "string" + }, + { + "type": "string" + } + ] + }, + "permission": { + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + }, + "minItems": 1 + }, + "obligation": { + "type": "array", + "items": { + "$ref": "#/definitions/Duty" + }, + "minItems": 1 + } + }, + "required": [ + "@id" + ] + }, + "MessageOffer": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/PolicyClass" + }, + { + "properties": { + "@type": { + "type": "string", + "const": "Offer" + } + } + }, + { + "anyOf": [ + { + "required": [ + "permission" + ] + }, + { + "required": [ + "prohibition" + ] + } + ] + } + ], + "required": [ + "@type" + ] + }, + "Offer": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/MessageOffer" + } + ], + "not": { + "required": [ + "target" + ] + } + }, + "Agreement": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/PolicyClass" + }, + { + "properties": { + "@type": { + "type": "string", + "const": "Agreement" + }, + "target": { + "type": "string" + }, + "assigner": { + "type": "string" + }, + "assignee": { + "type": "string" + }, + "timestamp": { + "type": "string", + "pattern": "-?([1-9][0-9]{3,}|0[0-9]{3})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])T(([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\\\\.[0-9]+)?|(24:00:00(\\\\.0+)?))(Z|(\\\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))?" + } + } + }, + { + "anyOf": [ + { + "required": [ + "permission" + ] + }, + { + "required": [ + "prohibition" + ] + } + ] + } + ], + "required": [ + "@type", + "target", + "assignee", + "assigner" + ] + }, + "Permission": { + "type": "object", + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/Constraint" + } + }, + "duty": { + "$ref": "#/definitions/Duty" + } + }, + "required": [ + "action" + ] + }, + "Duty": { + "type": "object", + "allOf": [ + { + "properties": { + "action": { + "$ref": "#/definitions/Action" + }, + "constraint": { + "type": "array", + "items": { + "$ref": "#/definitions/Constraint" + } + } + }, + "required": [ + "action" + ] + } + ] + }, + "Action": { + "type": "string" + }, + "Constraint": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/LogicalConstraint" + }, + { + "$ref": "#/definitions/AtomicConstraint" + } + ] + }, + "LogicalConstraint": { + "type": "object", + "properties": { + "and": { + "type": "array", + "items": "object" + }, + "andSequence": { + "type": "array", + "items": "object" + }, + "or": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/LogicalConstraint" + }, + { + "$ref": "#/definitions/AtomicConstraint" + } + ] + } + }, + "xone": { + "type": "array", + "items": "object" + } + }, + "oneOf": [ + { + "required": [ + "and" + ] + }, + { + "required": [ + "andSequence" + ] + }, + { + "required": [ + "or" + ] + }, + { + "required": [ + "xone" + ] + } + ] + }, + "AtomicConstraint": { + "type": "object", + "properties": { + "rightOperand": { + "$ref": "#/definitions/RightOperand" + }, + "leftOperand": { + "$ref": "#/definitions/LeftOperand" + }, + "operator": { + "$ref": "#/definitions/Operator" + } + }, + "required": [ + "rightOperand", + "operator", + "leftOperand" + ] + }, + "Operator": { + "type": "string", + "enum": [ + "eq", + "gt", + "gteq", + "lteq", + "hasPart", + "isA", + "isAllOf", + "isAnyOf", + "isNoneOf", + "isPartOf", + "lt", + "term-lteq", + "neq" + ] + }, + "RightOperand": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object" + }, + { + "type": "array" + } + ] + }, + "LeftOperand": { + "type": "string" + }, + "Reference": { + "type": "object", + "properties": { + "@id": { + "type": "string" + } + }, + "required": [ + "@id" + ] + } + } + } + """; +} diff --git a/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformerTest.java b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformerTest.java new file mode 100644 index 0000000..6ebe150 --- /dev/null +++ b/artifacts/buildSrc/src/test/java/org/eclipse/dsp/generation/transformer/HtmlTableTransformerTest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.dsp.generation.transformer; + +import org.eclipse.dsp.generation.jsom.ElementDefinition; +import org.eclipse.dsp.generation.jsom.SchemaProperty; +import org.eclipse.dsp.generation.jsom.SchemaPropertyReference; +import org.eclipse.dsp.generation.jsom.SchemaType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.dsp.generation.jsom.JsonTypes.ARRAY; +import static org.eclipse.dsp.generation.jsom.JsonTypes.STRING; + +class HtmlTableTransformerTest { + private HtmlTableTransformer transformer; + + @Test + void verifyTransform() { + var barType = new SchemaType("Bar"); + var fooType = new SchemaType("Foo"); + + var prop1 = SchemaProperty.Builder.newInstance() + .name("prop1") + .types(Set.of("#/definitions/Bar")) + .build(); + prop1.resolvedType(barType); + + var prop2 = SchemaProperty.Builder.newInstance() + .name("prop2") + .types(Set.of(STRING.getName())) + .build(); + prop2.resolvedType(STRING); + + var arrayElement = new ElementDefinition(ElementDefinition.Type.REFERENCE, "#/definitions/Bar"); + arrayElement.resolvedType(barType); + var prop3 = SchemaProperty.Builder.newInstance() + .name("prop3") + .types(Set.of(ARRAY.getName())) + .itemTypes(Set.of(arrayElement)) + .build(); + prop3.resolvedType(ARRAY); + + var propertyReference = new SchemaPropertyReference("prop3"); + propertyReference.resolvedProperty(prop3); + + fooType.properties(List.of(prop1, prop2, prop3)); + fooType.required(Set.of(propertyReference)); + + fooType.resolvePropertyReferences(); + barType.resolvePropertyReferences(); + + var result = transformer.transform(fooType); + + assertThat(result.contains("Foo<")).isTrue(); // verify type name + assertThat(result.contains("Required properties")).isTrue(); // verify required properties + assertThat(result.contains("Optional properties")).isTrue(); // verify optional properties + assertThat(result.contains("string")).isTrue(); // verify property type names are included + assertThat(result.contains("array[Bar]")).isTrue(); // verify array type names are included + } + + @BeforeEach + void setUp() { + transformer = new HtmlTableTransformer(); + } +} \ No newline at end of file