Skip to content

Commit ba02d48

Browse files
authored
Merge pull request #39 from plantbreeding/develop
Release 0.49
2 parents 5cb0a37 + 4c5e540 commit ba02d48

File tree

61 files changed

+3516
-2101
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3516
-2101
lines changed

java/cli/src/main/java/org/brapi/schematools/cli/GenerateSubCommand.java

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import org.brapi.schematools.core.openapi.generator.OpenAPIWriter;
2323
import org.brapi.schematools.core.openapi.generator.metadata.OpenAPIGeneratorMetadata;
2424
import org.brapi.schematools.core.openapi.generator.options.OpenAPIGeneratorOptions;
25+
import org.brapi.schematools.core.r.generator.RGenerator;
26+
import org.brapi.schematools.core.r.metadata.RGeneratorMetadata;
27+
import org.brapi.schematools.core.r.options.RGeneratorOptions;
2528
import org.brapi.schematools.core.response.Response;
2629
import org.brapi.schematools.core.sql.SQLGenerator;
2730
import org.brapi.schematools.core.sql.metadata.SQLGeneratorMetadata;
@@ -122,6 +125,21 @@ public void execute() throws IOException {
122125

123126
generateMarkdown(options);
124127
}
128+
case R -> {
129+
if (isNotGeneratingIntoSeparateFiles()) {
130+
handleError("The 'separate' option is not available for R file generation.");
131+
}
132+
RGeneratorOptions options = optionsPath != null ?
133+
RGeneratorOptions.load(optionsPath) : RGeneratorOptions.load();
134+
RGeneratorMetadata metadata = metadataPath != null ?
135+
RGeneratorMetadata.load(metadataPath) : RGeneratorMetadata.load();
136+
137+
if (overwrite != null) {
138+
options.setOverwrite(overwrite);
139+
}
140+
141+
generateR(options, metadata);
142+
}
125143
case SQL -> {
126144
if (isNotGeneratingIntoSeparateFiles()) {
127145
handleError("The 'separate' option is not available for SQL file generation.");
@@ -298,7 +316,6 @@ private void printOntModelErrors(Response<OntModel> response) {
298316
}
299317
}
300318

301-
302319
private void generateMarkdown(MarkdownGeneratorOptions options) {
303320
try {
304321
if (outputPath != null) {
@@ -344,6 +361,54 @@ private void printMarkdownErrors(Response<List<Path>> response) {
344361
}
345362
}
346363

364+
private void generateR(RGeneratorOptions options, RGeneratorMetadata metadata) {
365+
try {
366+
if (outputPath != null) {
367+
if (Files.isRegularFile(outputPath)) {
368+
handleError("For R generation the output path must be a directory");
369+
} else {
370+
371+
if (overwrite && Files.exists(outputPath)) {
372+
log.info("Overwriting existing R files in output directory '{}'", outputPath);
373+
deleteFiles(outputPath, metadata.getFilePrefix()) ;
374+
}
375+
376+
Files.createDirectories(outputPath);
377+
378+
RGenerator rGenerator = new RGenerator(options, outputPath);
379+
380+
Response<List<Path>> response = rGenerator.generate(schemaDirectory, metadata);
381+
382+
response.onSuccessDoWithResult(this::outputRPaths).onFailDoWithResponse(this::printRErrors);
383+
}
384+
} else {
385+
handleError("For R generation the output directory must be provided");
386+
}
387+
} catch (IOException exception) {
388+
handleException(exception);
389+
}
390+
}
391+
392+
private void outputRPaths(List<Path> paths) {
393+
if (paths.isEmpty()) {
394+
System.out.println("Did not generate any R files");
395+
} else if (paths.size() == 1) {
396+
System.out.println("Generated '1' R file:");
397+
System.out.println(paths.getFirst().toString());
398+
} else {
399+
System.out.printf("Generated '%s' R files:%n", paths.size());
400+
paths.forEach(path -> System.out.println(path.toString()));
401+
}
402+
}
403+
404+
private void printRErrors(Response<List<Path>> response) {
405+
if (response.getAllErrors().size() == 1) {
406+
printErrors("There was 1 error generating the R file(s)", response.getAllErrors());
407+
} else {
408+
printErrors(String.format("There were %d errors generating the R file(s)", response.getAllErrors().size()), response.getAllErrors());
409+
}
410+
}
411+
347412
private void generateSQL(SQLGeneratorOptions options, SQLGeneratorMetadata metadata) {
348413
try {
349414
if (outputPath != null) {
@@ -460,4 +525,14 @@ public static void deleteDirectoryRecursively(Path dir) throws IOException {
460525
}
461526
});
462527
}
528+
529+
public static void deleteFiles(Path dir, String prefix) throws IOException {
530+
try (var files = Files.list(dir)) {
531+
for (Path path : files.toList()) {
532+
if (Files.isRegularFile(path) && path.getFileName().toString().startsWith(prefix)) {
533+
Files.delete(path);
534+
}
535+
}
536+
}
537+
}
463538
}

java/cli/src/main/java/org/brapi/schematools/cli/OutputFormat.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ public enum OutputFormat {
2929
* Use this format to generate Markdown for type and their field descriptions
3030
*/
3131
MARKDOWN,
32-
33-
32+
/**
33+
* Use this format to generate R Client for types and their fields
34+
*/
35+
R,
3436
/**
3537
* Use this format to generate SQL for types and their fields
3638
*/

java/core/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies {
2020
implementation 'com.github.vertical-blank:sql-formatter:2.0.5'
2121
implementation 'org.dflib:dflib:2.0.0-M6'
2222
implementation 'org.dflib:dflib-json:2.0.0-M6'
23+
implementation 'org.thymeleaf:thymeleaf:3.1.2.RELEASE'
2324
}
2425

2526
mavenPublishing {

java/core/src/main/java/org/brapi/schematools/core/graphql/GraphQLGenerator.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@
1616
import org.brapi.schematools.core.utils.StringUtils;
1717

1818
import java.nio.file.Path;
19-
import java.util.ArrayList;
20-
import java.util.HashMap;
21-
import java.util.List;
22-
import java.util.Map;
23-
import java.util.Objects;
19+
import java.util.*;
2420
import java.util.function.Function;
2521
import java.util.stream.Collectors;
2622
import java.util.stream.Stream;
@@ -118,13 +114,13 @@ public Generator(GraphQLGeneratorOptions options, GraphQLGeneratorMetadata metad
118114
this.metadata = metadata;
119115

120116
this.brAPIClassCache = BrAPIClassCacheBuilder.createCache(brAPISchemas) ;
121-
objectOutputTypes = new HashMap<>();
122-
interfaceTypes = new HashMap<>();
123-
unionTypes = new HashMap<>();
124-
enumTypes = new HashMap<>();
125-
inputTypes = new HashMap<>();
126-
listResponseTypesToBeCreated = new HashMap<>();
127-
inputObjectTypeForListQueryToBeCreated = new HashMap<>();
117+
objectOutputTypes = new TreeMap<>();
118+
interfaceTypes = new TreeMap<>();
119+
unionTypes = new TreeMap<>();
120+
enumTypes = new TreeMap<>();
121+
inputTypes = new TreeMap<>();
122+
listResponseTypesToBeCreated = new TreeMap<>();
123+
inputObjectTypeForListQueryToBeCreated = new TreeMap<>();
128124
}
129125

130126
public Response<GraphQLSchema> generate() {
@@ -213,7 +209,7 @@ private Response<GraphQLSchema> createSchema(List<GraphQLObjectType> primaryType
213209

214210
builder.additionalType(createListResponse(paged, type));
215211
} else {
216-
log.warn(String.format("Can not create '%s' no type '%s'", key, value));
212+
log.warn("Can not create '{}' no type '{}'", key, value);
217213
}
218214
});
219215

@@ -223,7 +219,7 @@ private Response<GraphQLSchema> createSchema(List<GraphQLObjectType> primaryType
223219
if (type != null) {
224220
createInputObjectTypeForListQuery(type).onSuccessDoWithResult(builder::additionalType);
225221
} else {
226-
log.warn(String.format("Can not create '%s' no type '%s'", key, value));
222+
log.warn("Can not create '{}' no type '{}'", key, value);
227223
}
228224
});
229225

java/core/src/main/java/org/brapi/schematools/core/model/BrAPIPrimitiveType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class BrAPIPrimitiveType implements BrAPIType {
1616
/**
1717
* Boolean Integer Type
1818
*/
19-
public static final String INTEGER =" integer";
19+
public static final String INTEGER = "integer";
2020
/**
2121
* Boolean Number Type
2222
*/
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package org.brapi.schematools.core.r.generator;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.brapi.schematools.core.brapischema.BrAPISchemaReader;
6+
import org.brapi.schematools.core.model.BrAPIClass;
7+
import org.brapi.schematools.core.model.BrAPIObjectProperty;
8+
import org.brapi.schematools.core.model.BrAPIObjectType;
9+
import org.brapi.schematools.core.r.metadata.RGeneratorMetadata;
10+
import org.brapi.schematools.core.r.options.RGeneratorOptions;
11+
import org.brapi.schematools.core.response.Response;
12+
import org.brapi.schematools.core.utils.BrAPIClassCacheBuilder;
13+
import org.brapi.schematools.core.utils.StringUtils;
14+
import org.thymeleaf.TemplateEngine;
15+
import org.thymeleaf.context.Context;
16+
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
17+
18+
import java.io.IOException;
19+
import java.io.PrintWriter;
20+
import java.nio.charset.Charset;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
26+
import static org.brapi.schematools.core.response.Response.fail;
27+
import static org.brapi.schematools.core.response.Response.success;
28+
/**
29+
* Generates R Client from a BrAPI JSON Schema.
30+
*/
31+
@Slf4j
32+
@AllArgsConstructor
33+
public class RGenerator {
34+
private final BrAPISchemaReader schemaReader ;
35+
private final RGeneratorOptions options ;
36+
private final Path outputPath ;
37+
private final String commentPrefix = "# " ;
38+
39+
private final TemplateEngine templateEngine = createTemplateEngine();
40+
41+
/**
42+
* Creates a RGenerator using a default {@link BrAPISchemaReader} and
43+
* the default {@link RGeneratorOptions}.
44+
* @param outputPath the path of the output file or directory
45+
*/
46+
public RGenerator(Path outputPath) {
47+
this(new BrAPISchemaReader(), RGeneratorOptions.load(), outputPath) ;
48+
}
49+
50+
/**
51+
* Creates a RGenerator using a default {@link BrAPISchemaReader} and
52+
* the provided {@link RGeneratorOptions}.
53+
* @param options The options to be used in the generation.
54+
* @param outputPath the path of the output file or directory
55+
*/
56+
public RGenerator(RGeneratorOptions options, Path outputPath) {
57+
this(new BrAPISchemaReader(), options, outputPath) ;
58+
}
59+
60+
/**
61+
* Generates SQL files for type and their field descriptions
62+
* from the complete BrAPI Specification in
63+
* a directory contains a subdirectories for each module that contain
64+
* the BrAPI JSON schema and the additional subdirectories called 'Requests'
65+
* that contains the request schemas and BrAPI-Common that contains common schemas
66+
* for use across modules.
67+
* @param schemaDirectory the path to the complete BrAPI Specification
68+
* @return the paths of the Markdown files generated from the complete BrAPI Specification
69+
*/
70+
public Response<List<Path>> generate(Path schemaDirectory, RGeneratorMetadata metadata) {
71+
return schemaReader.readDirectories(schemaDirectory)
72+
.mapResultToResponse(brAPISchemas -> new Generator(brAPISchemas, metadata).generate()) ;
73+
}
74+
75+
private class Generator {
76+
private final BrAPIClassCacheBuilder.BrAPIClassCache brAPIClassCache;
77+
private final RGeneratorMetadata metadata ;
78+
public Generator(List<BrAPIClass> brAPIClasses, RGeneratorMetadata metadata) {
79+
this.brAPIClassCache = BrAPIClassCacheBuilder.createCache(brAPIClasses) ;
80+
this.metadata = metadata ;
81+
}
82+
83+
public Response<List<Path>> generate() {
84+
try {
85+
Files.createDirectories(outputPath) ;
86+
87+
List<BrAPIClass> entityClasses = brAPIClassCache.getBrAPICClassesAsList()
88+
.stream()
89+
.filter(this::isGenerating).toList();
90+
91+
List<Path> paths = new ArrayList<>();
92+
93+
return createBrAPIClient(entityClasses)
94+
.onSuccessDoWithResult(paths::add)
95+
.map(() -> entityClasses
96+
.stream()
97+
.map(this::createR6ClassForModel)
98+
.collect(Response.mergeLists()))
99+
.onSuccessDoWithResult(paths::addAll)
100+
.map(() -> success(paths));
101+
} catch (Exception e) {
102+
return fail(Response.ErrorType.VALIDATION, e.getMessage()) ;
103+
}
104+
}
105+
106+
private boolean isGenerating(BrAPIClass brAPIClass) {
107+
return brAPIClass instanceof BrAPIObjectType && brAPIClass.getMetadata() != null &&
108+
brAPIClass.getMetadata().isPrimaryModel() && options.isGeneratingFor(brAPIClass);
109+
}
110+
111+
private Response<Path> createBrAPIClient(List<BrAPIClass> entityClasses) {
112+
Context context = new Context();
113+
114+
context.setVariable("classNames", entityClasses.stream().map(options::getPluralFor).toList());
115+
context.setVariable("functionNames", entityClasses.stream().map(options::getPluralFor).map(StringUtils::toParameterCase).toList());
116+
context.setVariable("entityNames", entityClasses.stream().map(BrAPIClass::getName).toList());
117+
118+
String text = templateEngine.process("BrAPIClient.txt", context) ;
119+
120+
return writeToFile(outputPath.resolve(metadata.getFilePrefix() + "BrAPIClient.R"), "BrAPIClient", text) ;
121+
}
122+
123+
private Response<List<Path>> createR6ClassForModel(BrAPIClass brAPIClass) {
124+
if (brAPIClass instanceof BrAPIObjectType brAPIObjectType) {
125+
Context context = new Context();
126+
context.setVariable("className", options.getPluralFor(brAPIClass));
127+
context.setVariable("entityPath", options.getPathItemNameFor(brAPIClass));
128+
context.setVariable("searchPath", options.getSearchPathItemNameFor(brAPIClass));
129+
context.setVariable("entityName", brAPIObjectType.getName());
130+
context.setVariable("get-description", options.getSingleGet().getDescriptionFor(brAPIClass.getName()));
131+
context.setVariable("get", options.getSingleGet().isGeneratingFor(brAPIObjectType));
132+
context.setVariable("getAll", options.getListGet().isGeneratingFor(brAPIObjectType));
133+
context.setVariable("search", options.getSearch().isGeneratingFor(brAPIObjectType));
134+
context.setVariable("create", options.getPost().isGeneratingFor(brAPIObjectType));
135+
context.setVariable("update", options.getPut().isGeneratingFor(brAPIObjectType));
136+
context.setVariable("delete", options.getDelete().isGeneratingFor(brAPIObjectType));
137+
138+
BrAPIClass requestSchema = brAPIClassCache.getBrAPIClass(String.format("%sRequest", brAPIObjectType.getName()));
139+
140+
if (requestSchema instanceof BrAPIObjectType requestObjectType) {
141+
context.setVariable("requestArguments", requestObjectType.getProperties().stream().map(BrAPIObjectProperty::getName).toList()) ;
142+
context.setVariable("queryParameters", requestObjectType.getProperties().stream()
143+
.map(BrAPIObjectProperty::getName).map(options::getSingularForProperty).toList()) ;
144+
context.setVariable("argumentDescriptions", requestObjectType.getProperties().stream().map(this::getDescription).toList()) ;
145+
}
146+
147+
String text = templateEngine.process("EntityClass.txt", context) ;
148+
149+
return writeToFile(createPathForEntityClass(brAPIObjectType), brAPIObjectType.getName(), text)
150+
.mapResult(List::of) ;
151+
} else {
152+
return fail(Response.ErrorType.VALIDATION, brAPIClass.getName() + " is not a object class") ;
153+
}
154+
}
155+
156+
private String getDescription(BrAPIObjectProperty brAPIObjectProperty) {
157+
return StringUtils.extractFirstLine(brAPIObjectProperty.getDescription());
158+
}
159+
160+
private Path createPathForEntityClass(BrAPIObjectType brAPIObjectType) {
161+
return outputPath.resolve(metadata.getFilePrefix() + brAPIObjectType.getName() + ".R");
162+
}
163+
164+
private Response<Path> writeToFile(Path path, String name, String text) {
165+
try {
166+
if (!options.isOverwritingExistingFiles() && Files.exists(path)) {
167+
log.warn("Output file '{}' already exists and was not overwritten", path);
168+
return Response.empty() ;
169+
} else {
170+
Files.createDirectories(path.getParent()) ;
171+
172+
PrintWriter printWriter = new PrintWriter(Files.newBufferedWriter(path, Charset.defaultCharset()));
173+
174+
printWriter.print(commentPrefix) ;
175+
printWriter.println(name);
176+
177+
printWriter.println(text);
178+
179+
if (options.isAddingGeneratorComments()) {
180+
printWriter.println();
181+
printWriter.print(commentPrefix) ;
182+
printWriter.println("Generated by Schema Tools " + this.getClass().getSimpleName() + " Version: '" + options.getSchemaToolsVersion() +"'");
183+
}
184+
185+
printWriter.close();
186+
return success(path) ;
187+
}
188+
} catch (IOException exception){
189+
return fail(Response.ErrorType.VALIDATION, path, String.format("Can not write to file due to %s", exception.getMessage())) ;
190+
}
191+
}
192+
}
193+
194+
private TemplateEngine createTemplateEngine() {
195+
ClassLoaderTemplateResolver resolver = new ClassLoaderTemplateResolver();
196+
resolver.setPrefix("RTemplates/");
197+
resolver.setSuffix(".txt");
198+
resolver.setTemplateMode("TEXT");
199+
resolver.setCharacterEncoding("UTF-8");
200+
TemplateEngine engine = new TemplateEngine();
201+
engine.setTemplateResolver(resolver);
202+
return engine;
203+
}
204+
}

0 commit comments

Comments
 (0)