Skip to content

muukii/Grain

Repository files navigation

Grain

A data serialization template language in Swift

Describing data in Swift using DSL in .swift file then the command line application renders it into any format like JSON.

Motivation

In writing JSON or something else like that, we may be exhausted in describing repeatedly.
We often think somehow it could be done effectively like a programming language.
jsonnet is one of the preprocessors solving that.
Grain aims to achieve such a function in Swift language.

Create a swift file, then write data, and render it by using Grain tools.

The advantages of using Swift are:

  • writing data and data structure like thinking in SwiftUI
  • type checking by Swift compiler
  • validating the data

Inspireations of using Grain

  • Writing OpenAPI Specification and using by rendering
  • Writing JSON Schema
  • Writing a complex XcodeGen configuration beyond its built-in expressions
  • Managing mocking APNs files
  • and more there are a lot of opportunities of using Grain where are using JSON.

Naming

serialization -> cereal -> grain

Installation

From mint 🌱

$ mint install muukii/Grain

From make

$ make install

other installation ways will be added in the future

Overview

$ grain <template file>
$ grain <File 1> <File 2> ...
$ grain <File 1> <File 2> ... --output /path/to/output/

It writes results into given path using same name.
Schema.swift -> Schema.json

Instructions

A basic case

Creates Data.swift describing data

import GrainDescriptor

serialize {
  
  GrainObject {
    GrainMember("value") {
      1
    }
    
    for i in 0..<10 {
      GrainMember("key_\(i)") {
        i
      }
    }
  }
  
}

Renders Data.swift as JSON in Terminal

$ grain Data.swift
{
  "key_0" : 0,
  "key_1" : 1,
  "key_2" : 2,
  "key_3" : 3,
  "key_4" : 4,
  "key_5" : 5,
  "key_6" : 6,
  "key_7" : 7,
  "key_8" : 8,
  "key_9" : 9,
  "value" : 1
}

serialize function is the way to create output

serialize function declares what renders into data.
It also has implicit parameters to customize how to encode and how to save as files.

serialize {
  // describe what you serialize
}

output parameter accepts how result should be output.

serialize(output: .stdout) { ... }

prints out into stdout

serialize(output: .file) { ... }

write into file in the same directory of source, or specified output directory (using --output option)

serialize(output: .file(.named("<file_name>")) { ... }

writing file as well as above but uses different name by given name.

It allows us to define multiple times.
This makes files using given name.

serialize(output: .file(.named("pattern-1"))) {
  
}

serialize(output: .file(.named("pattern-2"))) {
  
}

Creating component and composing them to describe data efficiently

In Component.swift

import GrainDescriptor

serialize {
  
  GrainObject {
    GrainMember("data") {
      Results(records: [
        .init(name: "A", age: 1),
        .init(name: "B", age: 2),
      ])
    }
  }
  
}

// MARK: - Components

struct Record: GrainView {
  
  let name: String
  let age: Int
  
  var body: some GrainView {
    GrainObject {
      GrainMember("name") {
        name
      }
      GrainMember("age") {
        age
      }
    }
  }
  
}

struct Results: GrainView {
  
  let records: [Record]
  
  var body: some GrainView {
    GrainObject {
      GrainMember("results") {
        records
      }
    }
  }
  
}
$ grain Component.swift
{
  "data" : {
    "results" : [
      {
        "age" : 1,
        "name" : "A"
      },
      {
        "age" : 2,
        "name" : "B"
      }
    ]
  }
}

Use async/await

the template file allows writing async/await operation.
GrainDescriptor is including Alamofire as built-in.
For instance, we could create a serizalized data using networking like fetching data.

import GrainDescriptor

let response = try await AF.request("https://httpbin.org/get").serializingString().value

serialize {
  
  GrainObject {  
    GrainMember("result") {
      response
    }
  }
  
}

Showcases

OpenAPI Specification
import GrainDescriptor

serialize {
  Endpoint(methods: [
    .init(
      method: .get,
      summary: "Hello",
      description: "Hello Get Method",
      operationID: "id",
      tags: ["Awesome API"]
    )
  ])
}

// MARK: - Components

public struct Endpoint: GrainView {
  
  public var methods: [Method]
  
  public var body: some GrainView {
    GrainObject {
      for method in methods {
        GrainMember(method.method.rawValue) {
          method
        }
      }
    }
  }
}

public struct Method: GrainView {
  
  public enum HTTPMethod: String {
    case get
    case post
    case put
    case delete
  }
  
  public var method: HTTPMethod
  public var summary: String
  public var description: String
  public var operationID: String
  public var tags: [String]
  
  public var body: some GrainView {
    GrainObject {
      GrainMember("operationId") { operationID }
      GrainMember("description") { description }
      GrainMember("summary") { summary }
      GrainMember("tags") { tags }
    }
  }
}
$ grain endpoints.swift
{
  "get" : {
    "description" : "Hello Get Method",
    "operationId" : "id",
    "summary" : "Hello",
    "tags" : [
      "Awesome API"
    ]
  }
}