diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..dc391ff --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,17 @@ +name: Java CI + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + - name: Build with Maven + run: mvn --batch-mode --update-snapshots --no-transfer-progress clean test diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..7254456 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,27 @@ +name: Publish package to the Maven Central Repository +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Maven Central Repository + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'adopt' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + - id: install-secret-key + name: Install gpg secret key + run: | + cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG + - name: Publish package + run: mvn --batch-mode --no-transfer-progress "-Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSPHRASE }}" clean deploy + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bba7b53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target/ +/.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..83a8183 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ouest-France/SIPA Tech + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bf796e --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# bp-contract-generator-java + +Here is an easy and programmatic way to generate block provider contracts. Contracts are valid by design and will fit +perfectly with the BMS. + +This fluid API helps you to declare multiple block types with theirs endpoint, parameters and associated templates. + +You simply need to generate the object and let your favorite JSON framework serialize it. + +## How to install + +```xml + + + io.github.ouest-france + bp-contract-generator + 1.0.0 + +``` + +## Demonstration + +Here is an example with a block type that takes an input text, sends it to a remote service (for demonstration only), +and outputs the bolded text with different templates. + +```java + +@RequestMapping("/block-provider") +@RestController +@RequiredArgsConstructor +public class BlockProviderResource { + + @GetMapping("/configurations") + public ResponseEntity> configurations() { + return ResponseEntity.ok( + BlockProviderGenerator.create() + // the block type + .addType("bold").withVersion(1, 0, 0) + + // declare the endpoint url + .withEndpoint("https://localhost/block-configurations", "GET", "pure") + + // the input text + .addParameter("text") + .configurableAsString() + .required() + .withTitle("Text") + .withDescription("Text to display as bold") + + // send this parameter to the endpoint as a query parameter + .inEndpoint(QUERY, STRING) + .registerParameter() + + // first template to output using html tag + .addTemplate("html-tag") + .withSource("{{ text }}") + .registerTemplate() + + // second template to output using css style + .addTemplate("html-css") + .withSource("{{ text }}") + .registerTemplate() + + .registerType() + .generate() + ); + } + +} +``` + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..94f5b2e --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + io.github.ouest-france + bp-contract-generator + 1.0.0 + + + UTF-8 + UTF-8 + 8 + 8 + 2.8.9 + 5.1.0 + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter-engine.version} + test + + + + com.google.code.gson + gson + ${gson.version} + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.3.1 + + sipa.blockprovider.* + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + --pinentry-mode + loopback + + + + + + + + + + scm:git:${project.scm.url} + scm:git:${project.scm.url} + git@github.com:ouest-france/bp-contract-generator-java.git + HEAD + + + + + poussma + Mathieu POOUSSE + + + + + + MIT + https://opensource.org/licenses/MIT + repo + + + + bp-contract-generator-java + https://github.com/ouest-france/bp-contract-generator-java + A library to easily generate block provider contracts + + + + ossrh + Central Repository OSSRH + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + diff --git a/src/main/java/sipa/blockprovider/BlockProviderGenerator.java b/src/main/java/sipa/blockprovider/BlockProviderGenerator.java new file mode 100644 index 0000000..dde6a27 --- /dev/null +++ b/src/main/java/sipa/blockprovider/BlockProviderGenerator.java @@ -0,0 +1,49 @@ +package sipa.blockprovider; + +import sipa.blockprovider.domain.BlockType; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * The main class to generate block provider contracts. + */ +public class BlockProviderGenerator { + + /** + * Prepare a new block provider builder. + * + * @return see description + */ + public static BlockProviderGenerator create() { + return new BlockProviderGenerator(); + } + + private BlockProviderGenerator() { + + } + + private final List types = new ArrayList<>(); + + /** + * Add a block type to the block provider. + * + * @param name the name of the block type + * @return the builder + */ + public BlockTypeBuilder addType(final String name) { + final BlockTypeBuilder builder = new BlockTypeBuilder(this, name); + this.types.add(builder); + return builder; + } + + /** + * Finalize the block provider. + * + * @return a ready to serialize list of declared block types + */ + public List generate() { + return this.types.stream().map(type -> type.blockType).collect(Collectors.toList()); + } +} diff --git a/src/main/java/sipa/blockprovider/BlockTypeBuilder.java b/src/main/java/sipa/blockprovider/BlockTypeBuilder.java new file mode 100644 index 0000000..2036c7f --- /dev/null +++ b/src/main/java/sipa/blockprovider/BlockTypeBuilder.java @@ -0,0 +1,98 @@ +package sipa.blockprovider; + +import sipa.blockprovider.domain.BlockConfiguration; +import sipa.blockprovider.domain.BlockType; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * The class to generate block type. + */ +public class BlockTypeBuilder { + + final BlockType blockType; + private final BlockProviderGenerator owner; + BlockConfiguration blockConfiguration; + + BlockTypeBuilder(final BlockProviderGenerator owner, final String name) { + this.owner = owner; + this.blockType = new BlockType(this.blockConfiguration = new BlockConfiguration()); + this.blockType.setName(name); + } + + /** + * Finalize the block type. + * + * @return the block provider builder + */ + public BlockProviderGenerator registerType() { + if (this.blockConfiguration.getTemplates().size() == 0) { + // create default empty template + addTemplate("default").registerTemplate(); + } + return this.owner; + } + + /** + * This gives you a way to add / remove / replace custom properties to the section with the specified title. + * + * @param title the title of the section to customize + * @param consumer the consumer method that modifies the properties + * @return the block type builder + */ + public BlockTypeBuilder customizeSection(final String title, final Consumer> consumer) { + consumer.accept(this.blockConfiguration.getEndpoint().getUi().getByTitle(title)); + return this; + } + + /** + * Add a template with the specified name to the block type. + * + * @param name the template name + * @return the block type builder + */ + public TemplateBuilder addTemplate(final String name) { + return new TemplateBuilder(this, name); + } + + /** + * Set the version of the block type. + * + * @param major major version + * @param minor minor version + * @param patch patch number + * @return the block type builder + */ + public BlockTypeBuilder withVersion(final int major, final int minor, final int patch) { + this.blockConfiguration.setVersion(major + "." + minor + "." + patch); + return this; + } + + /** + * Specify the endpoint settings associated with the block type. + * + * @param url the url of the endpoint. This endpoint *MUST* be accessible by the BMS + * @param method the HTTP method to use when the BMS requests it (GET|POST|...) + * @param cachePolicy the cache policy to associate with the endpoint (pure|impure ~=~ 1h|1min) + * @return the block type builder + */ + public BlockTypeBuilder withEndpoint(final String url, final String method, final String cachePolicy) { + this.blockConfiguration.getEndpoint().setUrl(url); + this.blockConfiguration.getEndpoint().setMethod(method); + this.blockConfiguration.getEndpoint().setPure(Objects.equals(cachePolicy, "pure")); + return this; + } + + /** + * Add a parameter to the block type. + * + * @param name the parameter identifier + * @return the block type builder + */ + public ParameterBuilder addParameter(final String name) { + return new ParameterBuilder(this, name); + } + +} diff --git a/src/main/java/sipa/blockprovider/ParameterBuilder.java b/src/main/java/sipa/blockprovider/ParameterBuilder.java new file mode 100644 index 0000000..b5fca61 --- /dev/null +++ b/src/main/java/sipa/blockprovider/ParameterBuilder.java @@ -0,0 +1,362 @@ +package sipa.blockprovider; + +import sipa.blockprovider.domain.Endpoint; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * The class to generate parameter. + */ +public class ParameterBuilder { + + private final String name; + private String inSection; + private final BlockTypeBuilder owner; + private EndpointParameterBuilder endpointParameterBuilder; + private boolean required; + private String title; + private String description; + private String uiType; + private final Map additionalUIProperties = new LinkedHashMap<>(); + private String expressionModeDisabled; + + ParameterBuilder(final BlockTypeBuilder owner, final String name) { + this.owner = owner; + this.name = name; + } + + /** + * Finalize the parameter. + * + * @return the block type builder + */ + public BlockTypeBuilder registerParameter() { + if (this.endpointParameterBuilder != null) { + if (this.owner.blockConfiguration.getEndpoint().getUrl() == null) { + throw new IllegalStateException("you must define the endpoint first"); + } + this.owner.blockConfiguration.getEndpoint().getParameters().add( + new Endpoint.Parameter( + this.name, + this.endpointParameterBuilder.in.value, + this.required, + new Endpoint.Schema(this.endpointParameterBuilder.type.value, "string") + ) + ); + } + + final String sectionName = this.inSection != null ? this.inSection : "Configuration"; + + this.owner.blockConfiguration.getEndpoint().getUi().addProperty(sectionName, Map.of(this.name, toProperty())); + + if (this.expressionModeDisabled != null) { + this.owner.blockConfiguration.getEndpoint().getUi().disableExpressionMode(sectionName, this.name, this.expressionModeDisabled); + } + + if (this.required) { + this.owner.blockConfiguration.getEndpoint().getUi().addRequired(sectionName, this.name); + } + + this.owner.blockConfiguration.getEndpoint().getUi().order(sectionName, this.name); + + + return this.owner; + } + + private Map toProperty() { + LinkedHashMap result = new LinkedHashMap<>(); + result.put("title", this.title != null ? this.title : this.name); + result.put("type", this.uiType); + if (this.description != null) { + result.put("description", this.description); + } + result.putAll(this.additionalUIProperties); + return result; + } + + /** + * Set the title that will be displayed in the backoffice. + * + * @param title the title, if null or not set, the name will be used + * @return the parameter builder + */ + public ParameterBuilder withTitle(final String title) { + this.title = title; + return this; + } + + /** + * Set the section that holds this parameter. + * + * @param section the section title + * @return the parameter builder + */ + public ParameterBuilder inSection(final String section) { + this.inSection = section; + return this; + } + + /** + * Mark this parameter as required. + * + * @return the parameter builder + */ + public ParameterBuilder required() { + this.required = true; + return this; + } + + /** + * Specify the way this parameter must be passed to the endpoint. + * + * @param in the location of the parameter + * @param type the parameter type (used for serialization) + * @return the parameter builder + */ + public ParameterBuilder inEndpoint(final EndpointParameterBuilder.In in, final EndpointParameterBuilder.Type type) { + this.endpointParameterBuilder = new EndpointParameterBuilder(in, type); + return this; + } + + /** + * Set the description that will be displayed in the backoffice. + * + * @param description the description + * @return the parameter builder + */ + public ParameterBuilder withDescription(final String description) { + this.description = description; + return this; + } + + /** + * Specify this parameter is configured as an enum in the backoffice. + * + * @param values the list of enum values + * @return the parameter builder + */ + public ParameterBuilder configurableAsEnum(final List values) { + this.uiType = "string"; + this.additionalUIProperties.put("enum", values); + return this; + } + + /** + * Specify this parameter is configured using a dedicated widget. + * + * @param widget the widget name + * @param version the widget version + * @param configuration the additional widget configuration + * @return the parameter builder + */ + public ParameterBuilder configurableAsWidget(final String widget, final String version, final Object configuration) { + this.uiType = "string"; + this.additionalUIProperties.put("x-ui-configuration", + Map.of("widget", + Map.of( + "name", widget, + "version", version, + "configuration", configuration + ) + ) + ); + return this; + } + + /** + * Specify this parameter is configured as a boolean in the backoffice. + * + * @param defaultValue the default value of this parameter + * @return the parameter builder + */ + public ParameterBuilder configurableAsBoolean(final boolean defaultValue) { + this.uiType = "boolean"; + this.additionalUIProperties.put("default", defaultValue); + return this; + } + + /** + * Specify this parameter is configured as a string in the backoffice. + * + * @return the parameter builder + */ + public ParameterBuilder configurableAsString() { + return configurableAsString(null); + } + + /** + * Specify this parameter is configured as a string in the backoffice. + * + * @param defaultValue the default value + * @return the parameter builder + */ + public ParameterBuilder configurableAsString(final String defaultValue) { + this.uiType = "string"; + if (defaultValue != null) { + this.additionalUIProperties.put("default", defaultValue); + } + return this; + } + + + /** + * Mark this parameter can not be configured through expression (aka SPEL), only hardcoded values are allowed. + * + * @param mode when the expression mode is disabled + * @return the parameter builder + */ + public ParameterBuilder disableExpressionMode(final ExpressionModeDisabled mode) { + this.expressionModeDisabled = mode.value; + return this; + } + + /** + * Specify this parameter is configured as a number (with decimals) in the backoffice. + * + * @return the parameter builder + */ + public ParameterBuilder configurableAsNumber() { + this.uiType = "number"; + return this; + } + + /** + * Specify this parameter is configured as an integer in the backoffice. + * + * @return the parameter builder + */ + public ParameterBuilder configurableAsInteger() { + this.uiType = "integer"; + return this; + } + + /** + * A flag to disable the expression mode for a parameter. + */ + public enum ExpressionModeDisabled { + /** + * In any case, the expression mode is disabled. + */ + ALWAYS("always"), + /** + * The expression mode is disabled only when the block has been set as the leading block. + */ + LEADING("leading"); + + private final String value; + + ExpressionModeDisabled(final String value) { + this.value = value; + } + + /** + * Get the value for serialization. + * + * @return see description + */ + public String getValue() { + return this.value; + } + } + + /** + * The enpoint parameter information. + */ + public static class EndpointParameterBuilder { + // inherit private String name; + private final In in; + private final Type type; + + EndpointParameterBuilder(final In in, final Type type) { + this.in = in; + this.type = type; + } + + /** + * How the parameter is transmitted to the endpoint. + */ + public enum In { + /** + * Passed in a header with name parameter-name, and the value serialized as a string. + */ + HEADER("header"), + /** + * Passed in a cookie with name parameter-name, and the value serialized as a string. + */ + COOKIE("cookie"), + /** + * Passed in the path. + * The endpoint url must have a {parameter-name} fragment (https://service/path/{parameter-name}/fetch). + * This fragment will be replaced by the parameter value serialized as a string. + */ + PATH("path"), + /** + * Passed as a query parameter with name parameter-name, and the value serialized as a string. + */ + QUERY("query"), + /** + * Passed in the body of the request. + * The body is composed of a json object with the parameter-name as top-level property, and the associated value. + */ + BODY("body"); + + private final String value; + + In(final String value) { + this.value = value; + } + + /** + * Get the value for serialization. + * + * @return see description + */ + public String getValue() { + return this.value; + } + } + + /** + * The type of parameter. + */ + public enum Type { + /** + * Serialize as an integer. + */ + INTEGER("integer"), + /** + * Serialize as a number. + */ + NUMBER("number"), + /** + * Serialize as a boolean. + */ + BOOLEAN("boolean"), + /** + * Serialize as an object. + */ + OBJECT("object"), + /** + * Serialize as a string. + */ + STRING("string"); + + private final String value; + + Type(final String value) { + this.value = value; + } + + /** + * Get the value for serialization. + * + * @return see description + */ + public String getValue() { + return this.value; + } + } + } +} diff --git a/src/main/java/sipa/blockprovider/TemplateBuilder.java b/src/main/java/sipa/blockprovider/TemplateBuilder.java new file mode 100644 index 0000000..4b98ade --- /dev/null +++ b/src/main/java/sipa/blockprovider/TemplateBuilder.java @@ -0,0 +1,187 @@ +package sipa.blockprovider; + +import sipa.blockprovider.domain.Assets; +import sipa.blockprovider.domain.Template; + +/** + * The class to generate template. + */ +public class TemplateBuilder { + + private final Template template; + private final BlockTypeBuilder owner; + + TemplateBuilder(final BlockTypeBuilder owner, final String name) { + this.owner = owner; + this.template = new Template(); + this.template.setName(name); + } + + /** + * Specify the position where this template will be outputted. + * + * @param position the position + * @return the template builder + */ + public TemplateBuilder withPosition(final Position position) { + this.template.setPosition(position.value); + return this; + } + + /** + * Specify the template source code to render. + * + * @param source the mustache source + * @return the template builder + */ + public TemplateBuilder withSource(final String source) { + this.template.setSource(source); + return this; + } + + /** + * Specify if the template purpose is for regular HTML pages or for AMP. + * + * @param targetFormat the target format + * @return the template builder + */ + public TemplateBuilder withTargetFormat(final TargetFormat targetFormat) { + this.template.setTargetFormat(targetFormat.value); + return this; + } + + /** + * Add a shared javascript file url. + * + * @param url the url + * @return the template builder + */ + public TemplateBuilder withSharedJsUrl(final String url) { + initAssets(); + this.template.getAssets().getSharedJs().add(url); + return this; + } + + /** + * Add a shared css file url. + * + * @param url the url + * @return the template builder + */ + public TemplateBuilder withSharedCssUrl(final String url) { + initAssets(); + this.template.getAssets().getSharedCss().add(url); + return this; + } + + private void initAssets() { + if (this.template.getAssets() == null) { + this.template.setAssets(new Assets()); + } + } + + /** + * Add a javascript file url. + * + * @param url the url + * @return the template builder + */ + public TemplateBuilder withJsUrl(final String url) { + initAssets(); + this.template.getAssets().getJs().add(url); + return this; + } + + /** + * Add a css file url. + * + * @param url the url + * @return the template builder + */ + public TemplateBuilder withCssUrl(final String url) { + initAssets(); + this.template.getAssets().getCss().add(url); + return this; + } + + /** + * Finalize the template. + * + * @return the block type builder + */ + public BlockTypeBuilder registerTemplate() { + this.owner.blockConfiguration.getTemplates().add(this.template); + return this.owner; + } + + + /** + * The position where the template will be outputted. + */ + public enum Position { + /** + * In the middle of the body. + */ + PLACEHOLDER("placeholder"), + /** + * At the top of the head tag. + */ + HEAD_TOP("head-top"), + /** + * At the bottom of the head tag. + */ + HEAD_BOTTOM("head-bottom"), + /** + * At the top of the body tag. + */ + BODY_TOP("body-top"), + /** + * At the bottom of the body tag. + */ + BODY_BOTTOM("body-bottom"); + + private final String value; + + Position(final String value) { + this.value = value; + } + + /** + * Get the value for serialization. + * + * @return see description + */ + public String getValue() { + return this.value; + } + } + + /** + * The target format of the template. + */ + public enum TargetFormat { + /** + * Used for regular HTML pages. + */ + HTML("html"), + /** + * Used for AMP pages. + */ + AMP("amp"); + + private final String value; + + TargetFormat(final String value) { + this.value = value; + } + + /** + * Get the value for serialization. + * + * @return see description + */ + public String getValue() { + return this.value; + } + } +} diff --git a/src/main/java/sipa/blockprovider/domain/Assets.java b/src/main/java/sipa/blockprovider/domain/Assets.java new file mode 100644 index 0000000..2f19438 --- /dev/null +++ b/src/main/java/sipa/blockprovider/domain/Assets.java @@ -0,0 +1,31 @@ +package sipa.blockprovider.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Internal use only, do not interact. + */ +public class Assets { + + private final List sharedJs = new ArrayList<>(); + private final List sharedCss = new ArrayList<>(); + private final List js = new ArrayList<>(); + private final List css = new ArrayList<>(); + + public List getSharedJs() { + return this.sharedJs; + } + + public List getSharedCss() { + return this.sharedCss; + } + + public List getJs() { + return this.js; + } + + public List getCss() { + return this.css; + } +} diff --git a/src/main/java/sipa/blockprovider/domain/BlockConfiguration.java b/src/main/java/sipa/blockprovider/domain/BlockConfiguration.java new file mode 100644 index 0000000..56ebede --- /dev/null +++ b/src/main/java/sipa/blockprovider/domain/BlockConfiguration.java @@ -0,0 +1,30 @@ +package sipa.blockprovider.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Internal use only, do not interact. + */ +public class BlockConfiguration { + + private String version; + private final Endpoint endpoint = new Endpoint(); + private final List