Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON Schema for test code #211

Merged
merged 5 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 37 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ See [CreekService.org](https://www.creekservice.org) for info on Creek Service.
| 6.4.+ | 6.4 | Supported & tested |
| 6.4+ | 6.9.4 | Supported & tested |
| 7.+ | 7.6.1 | Supported & tested |
| 8.+ | 8.0.2 | Supported & tested |
| > 8.0.2 | | Not currently tested. Should work... |
| 8.+ | 8.5 | Supported & tested |
| > 8.5 | | Not currently tested. Should work... |

## Usage

Expand All @@ -32,7 +32,7 @@ See the portal for instructions on how to add the plugin to your build.

The JSON Schema plugin adds the following tasks to your project:

### generateJsonSchema - `GenerateJsonSchema`[5]
### generateJsonSchema - [GenerateJsonSchema][5]

> ### NOTE
> Details of how to annotate classes to control their schema can be found in the [Creek JSON Schema Generator docs][1].
Expand All @@ -44,8 +44,8 @@ The JSON Schema plugin adds the following tasks to your project:
*Dependants:* `processResources`

The `generateJsonSchema` task searches the class and module path for with [`@GeneratesSchema`][2] and write out
JSON schemas for them in YAML. The generated schemas are added to the `main` resource set, meaning they will be included
in any generated jar.
JSON schemas for them in YAML. The generated schema output directory is added to the `main` source set,
as an additional resource directories, meaning the schema will be included in any generated jar.

Types can be annotated both with [Jackson][3] and [JsonSchema][4] annotations, allowing control of the generated schema.

Expand All @@ -71,6 +71,25 @@ For example, the following limits the class & module path scanning to only two m
--type-scanning-allowed-module=acme.sales.model
```

### generateTestJsonSchema - [GenerateJsonSchema][5]

> ### NOTE
> Details of how to annotate classes to control their schema can be found in the [Creek JSON Schema Generator docs][1].

> ### NOTE
> Restricting class & module path scanning by setting allowed modules and allowed packages can increase the speed of your build.

*Dependencies:* `compileTestJava`, `compileTestKotlin`, `compileTestGroovy` if they exist.
*Dependants:* `processTestResources`

The `generateTestJsonSchema` works the same as [generateJsonSchema](#generatejsonschema---generatejsonschema5), only
for test code. The generated schema output directory is added to the `test` source set,
as an additional resource directories, meaning the schema will be available during unit testing.

**NOTE**: due to a [bug](https://github.com/java9-modularity/gradle-modules-plugin/issues/227) in the `org.javamodularity.moduleplugin` Gradle plugin,
generated resources are NOT patched in to the module during unit testing.
This should be fixed once https://github.com/java9-modularity/gradle-modules-plugin/pull/228 is merged and released.

### clean*TaskName* - `Delete`

Deletes the files created by the specified task. For example, `cleanGenerateJsonSchema` will delete the generated JSON schema files.
Expand Down Expand Up @@ -270,22 +289,28 @@ creek.schema.json {
## JSON Schema Generation

The `generateJsonSchema` task generates YAML files containing the JSON schema of each `@GeneratesSchema` annotated type
it encounters. By default, these are written to `$buildDir/generated/resources/schema/schema/json`, with
`$buildDir/generated/resources/schema` being added as a resource root. This means the schema files generated will
it encounters. By default, these are written to `$buildDir/generated/resources/schema/main/schema/json`, with
`$buildDir/generated/resources/schema/main` being added as a resource root. This means the schema files generated will
be included in the jar under a `schema/json` directory.

The `generateTestJsonSchema` task outputs to the `$buildDir/generated/resources/schema/test/schema/json`, with
`$buildDir/generated/resources/schema/test` being added as a resource root.

### Changing schema file location

The location where schema files are written can be changed by changing either:

* `creek.schema.json.schemaResourceRoot`: the resource root for schema, defaulting to `$buildDir/generated/resources/schema`, or
* `creek.schema.json.outputDirectoryName`: the name of the subdirectory under `creek.schema.json.schemaResourceRoot` where schemas will be written,
and defining the relative path to the schema files within the resulting jar file.
* `creek.schema.json.schemaResourceRoot`: the resource root for schema, defaulting to `$buildDir/generated/resources/schema/main`, or
* `creek.schema.json.testSchemaResourceRoot`: the resource root for test schema, defaulting to `$buildDir/generated/resources/schema/test`, or
* `creek.schema.json.outputDirectoryName`: the name of the subdirectory under `creek.schema.json.schemaResourceRoot` and
`creek.schema.json.testSchemaResourceRoot` where schemas will be written,
and defining the relative path to the schema files within the resulting jar file.

##### Groovy: Customising schema file output location
```groovy
creek.schema.json {
schemaResourceRoot = file("$buildDir/custom/build/path")
testSchemaResourceRoot = file("$buildDir/custom/build/path/for/test/schema")
outputDirectoryName = "custom/path/within/jar"
}
```
Expand All @@ -294,14 +319,15 @@ creek.schema.json {
```kotlin
creek.schema.json {
schemaResourceRoot.set(file("$buildDir/custom/build/path"))
testSchemaResourceRoot.set(file("$buildDir/custom/build/path/for/test/schema"))
outputDirectoryName.set("custom/path/within/jar")
}
```

## JVM Language support

Currently, the plugin automatically configures tasks to work with the standard Java, Groovy and Kotlin plugins.
Schema generation tasks are configured to read the output of `compileJava`, `compileGroovy` and `compileKotlin`
Schema generation tasks are configured to read the output of `compile[Test]Java`, `compile[Test]Groovy` and `compile[Test]Kotlin`
tasks if they are present in the project.

Support for other JVM languages may be added later, or you may be able to configure your own instance of `GenerateJsonSchema`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,17 @@ public TypeScanningSpec getSubtypeScanning() {
public abstract DirectoryProperty getSchemaResourceRoot();

/**
* Optional name of the directory under the {@link #getSchemaResourceRoot() resource root} where
* the schema will be written.
* Optional resource root where generated test schemas should be stored
*
* <p>Default: {@code $buildDir/generated/test-resources/schema}
*
* @return the test resource root property.
*/
public abstract DirectoryProperty getTestSchemaResourceRoot();

/**
* Optional name of the directory under the {@link #getSchemaResourceRoot() resource root} and
* {@link #getTestSchemaResourceRoot() test resource root} where the schema will be written.
*
* <p>This corresponds to the directory under which schema files will be located within the
* compiled jar file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,17 @@

import static org.creekservice.api.json.schema.gradle.plugin.GeneratorVersion.defaultGeneratorVersion;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.creekservice.api.json.schema.gradle.plugin.task.GenerateJsonSchema;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.plugins.BasePlugin;
import org.gradle.api.plugins.ExtensionAware;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaPlugin;
Expand All @@ -54,11 +53,17 @@ public final class JsonSchemaPlugin implements Plugin<Project> {
/** Generate schema task name. */
public static final String GENERATE_SCHEMA_TASK_NAME = "generateJsonSchema";

/** Generate test schema task name. */
public static final String GENERATE_TEST_SCHEMA_TASK_NAME = "generateTestJsonSchema";

/** Standard Creek group name. */
public static final String GROUP_NAME = "creek";

/** Default resource root */
public static final String DEFAULT_RESOURCE_ROOT = "generated/resources/schema";
public static final String DEFAULT_RESOURCE_ROOT = "generated/resources/schema/main";

/** Default test resource root */
public static final String DEFAULT_TEST_RESOURCE_ROOT = "generated/resources/schema/test";

/** Default output folder under the resource root. */
public static final String DEFAULT_OUTPUT_FOLDER = "schema/json";
Expand All @@ -74,12 +79,12 @@ public final class JsonSchemaPlugin implements Plugin<Project> {

@Override
public void apply(final Project project) {
project.getPluginManager().apply(BasePlugin.class);
project.getPluginManager().apply(JavaPlugin.class);

final JsonSchemaExtension extension = registerExtension(project);
registerGenerateSchemaTask(project, extension);
registerGenerateTestSchemaTask(project, extension);
registerJsonSchemaConfiguration(project);
project.afterEvaluate(this::afterEvaluate);
}

private JsonSchemaExtension registerExtension(final Project project) {
Expand All @@ -97,6 +102,10 @@ private JsonSchemaExtension registerExtension(final Project project) {
extension
.getSchemaResourceRoot()
.convention(project.getLayout().getBuildDirectory().dir(DEFAULT_RESOURCE_ROOT));
extension
.getTestSchemaResourceRoot()
.convention(
project.getLayout().getBuildDirectory().dir(DEFAULT_TEST_RESOURCE_ROOT));
extension.getOutputDirectoryName().convention(DEFAULT_OUTPUT_FOLDER);
extension.getExtraArguments().convention(List.of());
return extension;
Expand All @@ -107,6 +116,49 @@ private void registerGenerateSchemaTask(
final GenerateJsonSchema task =
project.getTasks().create(GENERATE_SCHEMA_TASK_NAME, GenerateJsonSchema.class);

task.getSchemaResourceRoot().set(extension.getSchemaResourceRoot());

configure(extension, task, SourceSet.MAIN_SOURCE_SET_NAME);

project.afterEvaluate(
proj ->
afterEvaluate(
proj,
GENERATE_SCHEMA_TASK_NAME,
JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME,
List.of(
JavaPlugin.COMPILE_JAVA_TASK_NAME,
"compileKotlin",
"compileGroovy"),
JavaPlugin.PROCESS_RESOURCES_TASK_NAME));
}

private void registerGenerateTestSchemaTask(
final Project project, final JsonSchemaExtension extension) {
final GenerateJsonSchema task =
project.getTasks().create(GENERATE_TEST_SCHEMA_TASK_NAME, GenerateJsonSchema.class);

task.getSchemaResourceRoot().set(extension.getTestSchemaResourceRoot());

configure(extension, task, SourceSet.TEST_SOURCE_SET_NAME);

project.afterEvaluate(
proj ->
afterEvaluate(
proj,
GENERATE_TEST_SCHEMA_TASK_NAME,
JavaPlugin.TEST_COMPILE_CLASSPATH_CONFIGURATION_NAME,
List.of(
JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME,
"compileTestKotlin",
"compileTestGroovy"),
JavaPlugin.PROCESS_TEST_RESOURCES_TASK_NAME));
}

private static void configure(
final JsonSchemaExtension extension,
final GenerateJsonSchema task,
final String sourceSetName) {
task.setGroup(GROUP_NAME);
task.getTypeScanningModuleWhiteList().set(extension.getTypeScanning().getModuleWhiteList());
task.getTypeScanningPackageWhiteList()
Expand All @@ -115,9 +167,17 @@ private void registerGenerateSchemaTask(
.set(extension.getSubtypeScanning().getModuleWhiteList());
task.getSubtypeScanningPackageWhiteList()
.set(extension.getSubtypeScanning().getPackageWhiteListed());
task.getSchemaResourceRoot().set(extension.getSchemaResourceRoot());
task.getOutputDirectoryName().set(extension.getOutputDirectoryName());
task.getExtraArguments().set(extension.getExtraArguments());

final SourceSetContainer sourceSetContainer =
task.getProject().getExtensions().findByType(SourceSetContainer.class);
if (sourceSetContainer == null) {
throw new IllegalStateException("source set container not registered");
}

final SourceSet sourceSet = sourceSetContainer.getByName(sourceSetName);
sourceSet.getOutput().dir(Map.of("buildBy", task.getName()), task.getSchemaResourceRoot());
}

private void registerJsonSchemaConfiguration(final Project project) {
Expand All @@ -142,32 +202,23 @@ private void registerJsonSchemaConfiguration(final Project project) {
.configureEach(task -> task.getGeneratorDeps().from(cfg));
}

private void afterEvaluate(final Project project) {
private void afterEvaluate(
final Project project,
final String generateTaskName,
final String compileConfigName,
final List<String> compileTaskNames,
final String resourceTaskName) {
final GenerateJsonSchema generateTask =
(GenerateJsonSchema) project.getTasks().getByName(GENERATE_SCHEMA_TASK_NAME);

final SourceSetContainer sourceSetContainer =
project.getExtensions().findByType(SourceSetContainer.class);
if (sourceSetContainer != null) {
final SourceSet mainSourceSet =
sourceSetContainer.getByName(SourceSet.MAIN_SOURCE_SET_NAME);

mainSourceSet
.getOutput()
.dir(
Map.of("buildBy", GENERATE_SCHEMA_TASK_NAME),
generateTask.getSchemaResourceRoot());
}
(GenerateJsonSchema) project.getTasks().getByName(generateTaskName);

final Configuration compileClassPathConfig =
project.getConfigurations()
.findByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
project.getConfigurations().findByName(compileConfigName);

if (compileClassPathConfig != null) {
generateTask.getProjectDeps().from(compileClassPathConfig);
}

final Set<Task> compileTasks = compileTasks(project);
final Set<Task> compileTasks = collectTasks(project, compileTaskNames);

compileTasks.forEach(
compileTask -> {
Expand All @@ -177,12 +228,12 @@ private void afterEvaluate(final Project project) {

generateTask.onlyIf(t -> compileTasks.stream().anyMatch(Task::getDidWork));

project.getTasksByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME, false)
project.getTasksByName(resourceTaskName, false)
.forEach(processTask -> processTask.dependsOn(generateTask));
}

private Set<Task> compileTasks(final Project project) {
return Stream.of(JavaPlugin.COMPILE_JAVA_TASK_NAME, "compileKotlin", "compileGroovy")
private Set<Task> collectTasks(final Project project, final Collection<String> taskNames) {
return taskNames.stream()
.map(taskName -> project.getTasksByName(taskName, false))
.flatMap(Set::stream)
.collect(Collectors.toUnmodifiableSet());
Expand All @@ -200,13 +251,15 @@ private <T extends ExtensionAware> ExtensionAware ensureExtension(
}

/** Simple extendable `creek` extension */
@SuppressWarnings("NullableProblems")
public abstract static class CreekSpec implements ExtensionAware {

@Override
public abstract ExtensionContainer getExtensions();
}

/** Simple extendable `schema` extension */
@SuppressWarnings("NullableProblems")
public abstract static class SchemaSpec implements ExtensionAware {

@Override
Expand Down
Loading
Loading