Git Product home page Git Product logo

modelgen's Introduction

ModelGen 🎰

ModelGen is a command-line tool for generating models from JSON Schemas.

Build Status GitHub codecov

Why?

Models are usually boilerplate code, why not generate them and forget? It will save you time writing boilerplate and eliminate model errors as your application scales in complexity.

This means that adding a property to a data object is truly a one-line change — no copy-paste required. If you want to refactor all your models it is simple as changing the template and regenerate them.

We support the following languages:

  • Swift
  • Kotlin
  • Java

But you can add support to any other language with few lines of code.

How it works?

Unlike most of the model generators, it works with two files, the .json and .stencil so you have full control on how you want your models to look like.

The Models are defined in JSON, based on JSON Schema but not limited to, basically anything you add on schema you can use the template. It is an extensible and language-independent specification.

Examples?

Take a look at Example folder.

Requirements

  • Xcode 10.2+ and Swift 4.2+

Installation

Homebrew

Run the following command to install using homebrew:

$ brew tap hebertialmeida/ModelGen https://github.com/hebertialmeida/ModelGen.git
$ brew install ModelGen

Manually

Run the following commands to build and install manually:

$ git clone https://github.com/hebertialmeida/ModelGen.git
$ cd ModelGen
$ make install

Defining a schema

ModelGen takes a schema file as an input.

{
  "title": "Company",
  "type": "object",
  "description": "Definition of a Company",
  "identifier": "id",
  "properties": {
    "id": {"type": "integer"},
    "name": {"type": "string"},
    "logo": {"type": "string", "format": "uri"},
    "subdomain": {"type": "string"}
  },
  "required": ["id", "name", "subdomain"]
}

Defining a template

ModelGen takes a template to generate in the format you want.

//
//  {{ spec.title }}.swift
//  ModelGen
//
//  Generated by [ModelGen]: https://github.com/hebertialmeida/ModelGen
//  Copyright © {% now "yyyy" %} ModelGen. All rights reserved.
//

{% if spec.description %}
/// {{ spec.description }}
{% endif %}
public struct {{ spec.title }} {
{% for property in spec.properties %}
{% if property.doc %}
    /**
     {{ property.doc }}
     */
{% endif %}
    public let {{ property.name }}: {{ property.type }}{% if not property.required %}?{% endif %}
{% endfor %}

    // MARK: - Initializers

{% map spec.properties into params using property %}{{ property.name }}: {{ property.type }}{% if not property.required %}?{% endif %}{% endmap %}
    public init({{ params|join:", " }}) {
{% for property in spec.properties %}
        self.{{ property.name }} = {{ property.name }}
{% endfor %}
    }
}

// MARK: - Equatable

extension {{ spec.title }}: Equatable {
    static public func == (lhs: {{spec.title}}, rhs: {{spec.title}}) -> Bool {
{% for property in spec.properties %}
        guard lhs.{{property.name}} == rhs.{{property.name}} else { return false }
{% endfor %}
        return true
    }
}

Generating models

To make it easy you can create a .modelgen.yml

spec: ../Specs/
output: ./Model/
template: template.stencil
language: swift

And then:

$ modelgen

Without the .modelgen.yml file

Generate from a directory:

$ modelgen --spec ./Specs --template template.stencil --output ./Model

Generate a single file:

$ modelgen --spec company.json --template template.stencil --output Company.swift

Generated output

//
//  Company.swift
//  ModelGen
//
//  Generated by [ModelGen]: https://github.com/hebertialmeida/ModelGen
//  Copyright © 2019 ModelGen. All rights reserved.
//

/// Definition of a Company
public struct Company {
    public let id: Int
    public let logo: URL?
    public let name: String
    public let subdomain: String

    // MARK: - Initializers

    public init(id: Int, logo: URL?, name: String, subdomain: String) {
        self.id = id
        self.logo = logo
        self.name = name
        self.subdomain = subdomain
    }
}

// MARK: - Equatable

extension Company: Equatable {
    static public func == (lhs: Company, rhs: Company) -> Bool {
        guard lhs.id == rhs.id else { return false }
        guard lhs.logo == rhs.logo else { return false }
        guard lhs.name == rhs.name else { return false }
        guard lhs.subdomain == rhs.subdomain else { return false }
        return true
    }
}

Attributions

This tool is powered by:

The inital concept was based on Peter Livesey's pull request and inspired by plank from Pinterest.

If you want to contribute, don't hesitate to open an pull request.

License

ModelGen is available under the MIT license. See the LICENSE file.

modelgen's People

Contributors

4brunu avatar emersoncarpes avatar hebertialmeida avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

modelgen's Issues

Rake aborted: ModelGen requires Xcode 8.*

... , but we were not able to find it. If it's already installed update your Spotlight index with 'mdimport /Applications/Xcode*'

I am trying with Xcode 9, is Xcode 9 not supported?

JSON Key mapping?

Does ModelGen support JSON Key to properties mapping?

In the user.json example we see the integer property current_company_id, this snake cased name matches the JSON key but as part of the ModelGen parsing - using the method fixVariableName in Sources/Filters.swift (used in JsonParser+Context.swift) - the name of the variable becomes currentCompanyId as we see in the User.swiftfile:

self.currentCompanyId = try unboxer.unbox(key: "current_company_id")

It would be nice to be able to not use automatic transformation between JSON key and name of variable name. Let's say that I have a json key created_at but I would like to call the property creationDate (even though createdAt is more Swifty of course...) , is that supported? If not, that would be great!

Maybe the name of the JSON Key key be an "attribute" in the Specs, so in the created_at -> creationDate example above the specs would look something like:

  "properties": {
    ...,
    "creationDate": { "type": "string", "format": "date", "json_key": "created_at" },
    ...
   }

Would that be fairly simple to include?

Adding more info to Schema and JSONParser

In a fork of this repo I have added some more metadata helpful in our stencil templates,

In Schema:

  let isOptional: Bool
  let isMutable: Bool
  let jsonKey: String?
  var hasCustomJsonKey: Bool {
    return jsonKey != nil
  }

Inside JsonParser:

  public var customKeyProperties: [SchemaProperty] {
    return properties.filter { $0.hasCustomJsonKey }
  }

  public var nonCustomKeyProperties: [SchemaProperty] {
    return properties.filter { !$0.hasCustomJsonKey }
  }
  
  public var nonOptionalProperties: [SchemaProperty] {
    return properties.filter { !$0.isOptional }
  }
  
  public var nonOptionalProperties: [SchemaProperty] {
    return properties.filter { $0.isOptional }
  }
  
  public var immutableProperties: [SchemaProperty] {
    return properties.filter { !$0.isMutable }
  }
  
  public var mutableProperties: [SchemaProperty] {
    return properties.filter { $0.isMutable }
  }

I added these arrays allowing for filtering on properties since stencil does not support filtering. Only map.

Here are some use cases for these filtered properties;

CustomStringConvertible

If I declare a protocol in my template.stencil and let that protocol, let's call it MyModel as CustomStringConvertible, we can then give it a default implementation of var description: String that we can override. We don't want to print optional properties without unwrapping them, so we want to first iterate through all nonoptional properties and then unwrap all optional. So we need the nonOptionalProperties and the nonOptionalProperties computed properties, an ugly example of usage:

public extension {{ protocol }} {
    var description: String {
var nonOptional = ""
nonOptional = "{% for p in spec.nonOptionalProperties %}{{p.name}}: `\({{p.name}})`{% if not forloop.last %}, {% endif %}{% endfor %}"
{% if spec.optionalProperties.count > 0 %}
    {% for p in spec.optionalProperties %}
        if let value = {{ p.name }} {
            nonOptional.append(", {{ p.name }}: `\(value)`")
        }
    {% endfor %}
{% endif %}

return nonOptional
    }
}

CodingKeys

Or let's say we want to make MyModel conform to Codable with default implementations, then we want to separate between those properties who have a custom jsonKey and those who have not:

enum {{ codingKeys }}: String, CodingKey {

    //MARK: Custom JSON key cases
{% for customKeyCase in spec.customKeyProperties %}
    case {{ customKeyCase.name }} = "{{ customKeyCase.jsonKey }}"
{% endfor %}

    //MARK: Standard cases
{% if spec.nonCustomKeyProperties.count > 0 %}
    case {% for case in spec.nonCustomKeyProperties %}{{ case.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endif %}
}

Which result in e.g. this Swift code for a Hotel model:

enum HotelCodingKeys: String, CodingKey {

    //MARK: Custom JSON key cases
    case location = "geolocation"

    //MARK: Standard cases
    case name, propertyCode, image, phoneNumber, address
}

For this template:

{
  "title": "Hotel",
  "concreteType": "struct",
  "properties": {
    "name": {"type": "string"},
    "propertyCode": {"type": "string"},
    "image": {"type": "string"},
    "phoneNumber": {"type": "string"},
    "address": {"$ref": "address.json"},
    "location": {"$ref": "location.json", "jsonKey": "geolocation"}
  }
}

I have already implemented this in a fork, do you feel like this is something that you want part of this repo? If so I can create a PR?

Order of properties arbitrary?

The order of the properties is a bit strange. The order between the spec and the generated model is not the same. This is a bit problematic since we sometimes want to specify orders of the properties in our generated Swift files and the only way o

template.stencil

{% for p in spec.properties %}
    var {{ p.name }}: {{ p.type }} { get }
{% endfor %}
"properties": {
     "a": {"type": "integer"}
}

is trivial of course and results in:

var a: Int { get }

When we add a new property b below/after a, we expect b to be declared after a in the generated Swift file:

"properties": {
     "a": {"type": "integer"},
     "b": {"type": "integer"}
}

However, it results in the unexpected:

    var b: Int { get }
    var a: Int { get }

Hmm... "So maybe the order is reversed?" I thought, so continuing with a third property c:

"properties": {
      "a": {"type": "integer"},
      "b": {"type": "integer"},
      "c": {"type": "integer"}
}

Now it gets really interesting, this results in the super unexpected Swift code:

    var b: Int { get }
    var a: Int { get }
    var c: Int { get }

"So what if the order is completely random?" I thought, so I cleared the contents of the file and ran ModelGen like ten times. Same result, same order every time.

So what if we change a property, from type integer to string, what happens?

"properties": {
    "a": {"type": "integer"},
    "b": {"type": "string"},
    "c": {"type": "integer"}
}

The order did not change, still the unexpected and unwanted b, a, c:

    var b: String { get }
    var a: Int { get }
    var c: Int { get }

Adding a forth, d:

"properties": {
        "a": {"type": "integer"},
        "b": {"type": "string"},
        "c": {"type": "integer"},
        "d": {"type": "integer"},
}

Results in the unexpected:

    var b: String { get }
    var a: Int { get }
    var d: Int { get }
    var c: Int { get }

Adding a fifth e:

"properties": {
    "a": {"type": "integer"},
    "b": {"type": "string"},
    "c": {"type": "integer"},
    "d": {"type": "integer"},
    "e": {"type": "integer"}
}

Results in:

protocol RemoveThisModelTestingOnly {
    var b: String { get }
    var e: Int { get }
    var a: Int { get }
    var d: Int { get }
    var c: Int { get }
}

So the weird thing is that the same order seems to be occurring every time, but the order itself is completely arbitrary? At least I can see a pattern:

a
b, a
b, a, c
b, a, d, c
b, e, a, d, c

What is the cause of this? Because in JsonParser+Context it seems to be keeping the order in the method dicToArray:

	private func dicToArray() throws {
		guard let items = json["properties"] as? JSON else {
			throw JsonParserError.missingProperties
		}

		var properties = [JSON]()
		for (key, value) in items {
			guard let value = value as? JSON else {
				throw JsonParserError.missingProperties
			}
			var nValue = value
			nValue["name"] = key
			properties.append(nValue)
		}
		json["properties"] = properties
	}

The functions func mapProperties() and func prepareProperties(_ items: [JSON], language: Language) also seem to be keeping the order?

Property defaulting to `required` and support `optional` using SchemaProperty in property list?

Right now there is a code duplication in the specs, e.g. take a look at user.json example, where almost all property names occurs inside "properties" dictionary and also inside the "required" array.

Maybe whether or not a property is required/optional information can be included in the SchemaProperty? So for user.json it would change from:

{
  "title": "User",
  "type": "object",
  "description": "Definition of a User",
  "identifier": "id",
  "properties": {
    "id": {"type": "integer"},
    "full_name": {"type": "string"},
    "email": {"type": "string"},
    "timezone": {"type": "string"},
    "current_company_id": {"type": "integer"},
    "created_at": {"type": "string", "format": "date"},
    "companies": {
      "type": "array",
      "items": {"$ref": "company.json"}
    },
    "avatar": {"$ref": "avatar.json"}
  },
  "required": ["id", "full_name", "email", "current_company_id", "created_at", "companies", "avatar"]
}

to:

{
  "title": "User",
  "type": "object",
  "description": "Definition of a User",
  "identifier": "id",
  "properties": {
    "id": {"type": "integer"},
    "full_name": {"type": "string"},
    "email": {"type": "string"},
    "timezone": {"type": "string", "optional": true },
    "current_company_id": {"type": "integer"},
    "created_at": {"type": "string", "format": "date"},
    "companies": {
      "type": "array",
      "items": {"$ref": "company.json"}
    },
    "avatar": {"$ref": "avatar.json"}
  }}

Without any code duplication! Or am I missing something crucial here?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.