View on GitHub

xDDL

An eXtensible Data Definition Language

Generating Model Artifacts and Sharing Between Projects

Before We Start

We are not going to discuss the details of xDDL specifications here. While we like to think the code should be relatively self evident, if you would like to know more, please see the writing a specification documentation for details.

Table of Contents

  1. Generation and Plugins
  2. Project Setup
  3. Java Code Generation
    1. Complete List of Java Extension Properties
    2. Enums and the Java Plugin
  4. Swift Code Generation
    1. Complete List of Swift Extension Properties

Generation and Plugins

The primary way the xDDL utilities are used with with “generate” plugins. These generate new artifacts from your xDDL specification file.

The core plugins are:

But here we are going to focus on the two that generate source code for you: Java and Swift.

Project Setup

Basic code generation can be done on any xDDL model, and will generate a set of defaults based on the core types, but you can highly customize the sources that are generated. First, though, let’s set up a Gradle project:

build.grade

plugins {
    id "net.kebernet.xddl" version "+"
}

apply plugin: 'java'

repositories { jcenter() }

sourceSets {
    main {
        java.srcDirs([
                file("${project.buildDir}/xddl-java"),
                file("src/main/java")
        ])
    }
}

task glide(type: XDDLGlide){}

task glideJava(type: XDDLGlideGenerate, dependsOn: glide){
    plugin "java"
    outputDirectory file("${project.buildDir}/xddl-java")
}

compileJava.dependsOn glideJava

This will give us a basic Java project where the files in default project layout…

… will be generated into the ./build/xddl-java directory, and packaged into the JAR artifact for the project. You can learn more about the Glide plugin in the documentation for ElasticSearch.

If we create a empty Specification.xddl.json file (containing simply {}), we have a start. We can then create xddl/includes/Item.xddl.json as below:

{
  "@type": "Structure",
  "name": "Item",
  "properties": [
    {"@type": "Type", "core": "INTEGER", "name": "count"}
  ]
}

and we do gradle build, we will see the following file generated into the ./build/xddl.

package xddl;

public class Item {
  private Integer count;

  /**
   *
   * @return the value
   */
  public Integer getCount() {
    return this.count;
  }

  /**
   *
   * @param value the value
   */
  public void setCount(final Integer value) {
    this.count = value;
  }

  /**
   * @param value the value 
   * @return this
   */
  public Item count(final Integer value) {
    this.count = value;
    return this;
  }

  @Override
  public boolean equals(Object o) {
    if(!(o instanceof Item)) return false;
    Item that = (Item) o;
    return 
           java.util.Objects.equals(this.count,that.count) && 
              true;
  }

  @Override
  public int hashCode() {
    return   java.util.Objects.hash(
           this.count,
            0);
  }
}

Java Code Generation

While in the previous section we generated Java sources, the project setup was generally useful for any kind of source generation. Now we will look in detail at the Java plugin. You can see above our “INTEGER” property was created as a java.lang.Integer type on the generated Java code. Here is a list of the default mappings for each of the core xDDL types:

You can, override any of these defaults, however, with the Java extension:

 {
   "@type": "Structure",
   "name": "Item",
   "properties": [
     {
        "@type": "Type", "core": "DATETIME", "name": "theDate",
        "ext": {
          "java": {
            "type": "java.util.Date"          
          }       
        }     
      
     }
   ]
 }

You can also use the Java extension on the type to import types to use. For example

 {
   "@type": "Structure",
   "name": "Item",
   "ext": {
      "java": {
        "imports": ["javax.persistence.Entity", "javax.persistence.Temporal", "javax.persistence.TemporalType"],
        "annotations": "@Entity"
      }   
    },
   "properties": [
     {
        "@type": "Type", "core": "DATETIME", "name": "theDate",
        "ext": {
          "java": {
            "type": "java.util.Date",      
            "annotations": "@Temporal(TemporalType.TIMESTAMP)"    
          }       
        }     
      
     }
   ]
 }

Resulting in:

package xddl;

import javax.persistence.Entity;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class Item {
  @Temporal(TemporalType.TIMESTAMP)
  private Date theDate;

  /**
   *
   * @return the value
   */
  public Date getTheDate() {
    return this.theDate;
  }
//...

Complete List of Java Extension Properties

Enums and the Java Plugin

If you have “allowables” defined on a type, it will be treated as an enum, and only STRING types will be treated as enums. If any of the values are not usable as Java-ish names, they will be prefixed with val_ and Jackson @JsonCreator method. So for instance:

{
  "@type": "Type",
  "core": "STRING",
  "name": "ordinal_enum",
  "description": "A referenced enum property value",
  "allowable": [
    {"value": "1ST", "description": "The 1st value"},
    {"value": "SECOND", "description": "THe 2nd value"},
    {"value": "THIRD", "comment": "Maybe add a 3rd value?"}
  ]
}

Results in

 /**
 * A referenced enum property value
 */
public enum OrdinalEnum implements Serializable {
  /**
   * The 1st value
   */
  VAL_1ST("1st"),

  /**
   * THe 2nd value
   */
  SECOND("SECOND"),

  /**
   * Comment: Maybe add a 3rd value?
   */
  THIRD("THIRD");

  private final String value;

  OrdinalEnum(String value) {
    this.value = value;
  }

  @Override
  @JsonValue
  public String toString() {
    return value;
  }

  /**
   * @return enum value
   * @param value string value */
  @JsonCreator
  public static OrdinalEnum forValue(String value) {
    for (OrdinalEnum check : values()) {
      if(check.value.equals(value)) {
        return check;
      }
    }
    throw new IllegalArgumentException(value);
  }
}

Swift Code Generation

Swift Code generation works largely like the Java generation, except we don’t need to include the Swift code in our project (unless we want to). The Swift Plugin outputs an entire directory suitable for publishing as a SwiftPM module. The code generated by the Swift plugin is for use with Apple’s “Codable” API.

We first need to add a new task to our Gradle project:

task glideSwift(type: XDDLGlideGenerate, dependsOn: glide){
    plugin "swift"
    outputDirectory file("${project.buildDir}/swift")
}

This results in:

Pacakge.swift

// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "xddl",
  products: [
    .library(
      name: "xddl",
      targets: ["xddl"]
    )
  ],
  dependencies: [
  ],
  targets:[
    .target(
      name: "xddl",
      dependencies:[]),
  ]
)

and Sources/xddl/Item.swift

import Foundation

public struct Item: Codable {
  var count: Int?
}

Like the Java plugin, if you have named versions of your specification generated by the Glide plugin, you will get multiple targets in your Package.swift file, each named like “library_vX_X”.

Complete List of Swift Plugin Extensions