Skip to content

Commit

Permalink
Merge pull request #6 from tylerchr/generator
Browse files Browse the repository at this point in the history
Implement a JSTN generator and related APIs
  • Loading branch information
tylerchr committed Jun 16, 2018
2 parents 801464b + 8024c27 commit 9b04954
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 11 deletions.
18 changes: 14 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,20 @@ to the JSTN grammar.
A JSTN generator SHOULD provide mechanisms for generating the JSTN text in both
concise and pretty formats. The concise format omits all newlines (so object
pairs must be delineated by semicolons) but MAY include horizontal whitespace.
The pretty format includes newline characters (1) after each begin-object token
and (2) both prior to and after each end-object token. In the pretty format,
each line MUST be indented with an amount of whitespace corresponding to its
depth in the object hierarchy.
The pretty format differs from the concise format only in the following ways:

1. If an object type includes more than zero properties, the generator MUST render
a newline newline character (1) after each begin-object token and (2) prior to
each end-object token, and (3) after each end-object token.

2. When rendering object properties, the generator SHOULD render the name-separator
as a colon (%x3A) followed by a single space character (%x20).

2. Each line MUST be indented with an amount of whitespace proportional to its
depth in the object hierarchy. A specific whitespace string is not defined.

Note that any JSTN text with no non-empty objects renders identically in both the
concise and pretty formats.

Other valid formatting variations exist, and a JSTN generator MAY additionally
implement support for other such variations.
Expand Down
110 changes: 110 additions & 0 deletions generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package jstn

import (
"bytes"
"io"
"sort"
"strings"
)

// indentationString defines the whitespace string to use for indentation when
// generating using the pretty format.
const indentationString = " "

// Generate formats t into a JSTN type declaration using the concise format
// defined by the JSTN specification.
func Generate(t Type) ([]byte, error) {
return (generator{Pretty: false}).generate(t, 0), nil
}

// GeneratePretty formats t into a JSTN type declaration using the pretty
// format defined by the JSTN specification.
func GeneratePretty(t Type) ([]byte, error) {
return (generator{Pretty: true, Indentation: indentationString}).generate(t, 0), nil
}

type generator struct {
Pretty bool // Whether to render in pretty mode.
Indentation string // When in pretty mode, the indentation character to use.
}

// generate formats t into a JSTN document. Because it is a recursive function,
// depth tracks the object hierarchy depth for use when pretty-printing.
func (g generator) generate(t Type, depth int) []byte {

var buf bytes.Buffer

switch t.Kind {
case String:
io.WriteString(&buf, "string") // token: string

case Number:
io.WriteString(&buf, "number") // token: number

case Boolean:
io.WriteString(&buf, "boolean") // token: boolean

case Null:
io.WriteString(&buf, "null") // token: null

case Object:
io.WriteString(&buf, "{") // token: begin-object

// writePretty adds whitespace, but only if the generator is in pretty mode.
writePretty := func(s string) {
if g.Pretty && len(t.Properties) > 0 {
io.WriteString(&buf, s)
}
}

// Sort property names for determinism.
var propertyNames []string
for k := range t.Properties {
propertyNames = append(propertyNames, k)
}
sort.Strings(propertyNames)

writePretty("\n")
for i, k := range propertyNames {

// In pretty mode, indent the property declaration line.
writePretty(strings.Repeat(g.Indentation, depth+1))

// token: name
io.WriteString(&buf, k)

// token: name-separator
io.WriteString(&buf, ":")
writePretty(" ")

// token: member
buf.Write(g.generate(*t.Properties[k], depth+1))

// token: delimiter
writePretty("\n")
if !g.Pretty && i < len(propertyNames)-1 {
io.WriteString(&buf, ";")
}
}

writePretty(strings.Repeat(g.Indentation, depth))
io.WriteString(&buf, "}") // token: end-object

case Array:
io.WriteString(&buf, "[") // token: begin-array

// token: type-declaration
if t.Items != nil {
buf.Write(g.generate(*t.Items, depth))
}

io.WriteString(&buf, "]") // token: end-array
}

if t.Optional {
io.WriteString(&buf, "?") // token: value-optional
}

return buf.Bytes()

}
154 changes: 154 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package jstn

import "testing"

func TestGenerator(t *testing.T) {

cases := []struct {
Type Type
String string
Pretty bool
}{
{
Type: Type{Kind: String, Optional: false},
String: "string",
},
{
Type: Type{Kind: String, Optional: true},
String: "string?",
},
{
Type: Type{Kind: Number, Optional: false},
String: "number",
},
{
Type: Type{Kind: Number, Optional: true},
String: "number?",
},
{
Type: Type{Kind: Boolean, Optional: false},
String: "boolean",
},
{
Type: Type{Kind: Boolean, Optional: true},
String: "boolean?",
},
{
Type: Type{Kind: Null, Optional: false},
String: "null",
},
{
Type: Type{Kind: Null, Optional: true},
String: "null?",
},

//
// ARRAYS
//

{
// This case is actually not permitted by the current spec, but is
// proposed in https://github.com/tylerchr/jstn/issues/5.
Type: Type{Kind: Array, Optional: false, Items: nil},
String: "[]",
},
{
Type: Type{Kind: Array, Optional: false, Items: &Type{
Kind: String,
}},
String: "[string]",
},
{
// An array may be an optional type.
Type: Type{Kind: Array, Optional: true, Items: &Type{
Kind: String,
}},
String: "[string]?",
},
{
// An array may contain an optional type.
Type: Type{Kind: Array, Optional: false, Items: &Type{
Kind: String, Optional: true,
}},
String: "[string?]",
},

//
// OBJECTS
//

{
Type: Type{Kind: Object, Optional: false, Properties: nil},
String: "{}",
},
{
Type: Type{Kind: Object, Optional: false, Properties: nil},
String: "{}",
Pretty: true,
},
{
Type: Type{Kind: Object, Optional: false, Properties: map[string]*Type{
"firstName": &Type{Kind: String, Optional: false},
"age": &Type{Kind: Number, Optional: true},
}},
String: "{age:number?;firstName:string}",
},
{
Type: Type{Kind: Object, Optional: false, Properties: map[string]*Type{
"firstName": &Type{Kind: String, Optional: false},
"age": &Type{Kind: Number, Optional: true},
}},
String: `{
age: number?
firstName: string
}`,
Pretty: true,
},
{
Type: Type{Kind: Object, Optional: true, Properties: map[string]*Type{
"firstName": &Type{Kind: String, Optional: false},
"age": &Type{Kind: Number, Optional: true},
"residences": &Type{Kind: Array, Optional: false, Items: &Type{
Kind: Object, Optional: false, Properties: map[string]*Type{
"city": &Type{Kind: String, Optional: false},
"country": &Type{Kind: String, Optional: true},
},
}},
}},
String: `{
age: number?
firstName: string
residences: [{
city: string
country: string?
}]
}?`,
Pretty: true,
},
}

for i, c := range cases {

var out []byte
var err error

if c.Pretty {
out, err = GeneratePretty(c.Type)
} else {
out, err = Generate(c.Type)
}

if err != nil {
t.Errorf("[case %d] unexpected generator error: %s", i, err)
continue
}

if string(out) != c.String {
t.Errorf("[case %d] unexpected production:", i)
t.Errorf(". expected: %q", c.String)
t.Errorf(". got : %q", out)
}

}

}
16 changes: 9 additions & 7 deletions parser.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package jstn implements a reference parser and validator for JSON Type Notation.
// Package jstn implements a reference parser, validator, and generator for JSON Type Notation.
package jstn

import (
Expand All @@ -7,6 +7,14 @@ import (
"strings"
)

// Parse parses a JSTN text into a native representation.
func Parse(schema string) (Type, error) {
r := strings.NewReader(schema)
p := &parser{s: newScanner(r)}
return p.Parse()
}

// MustParse is equivalent to Parse, except that it panics if the text cannot be parsed.
func MustParse(schema string) Type {
t, err := Parse(schema)
if err != nil {
Expand All @@ -15,12 +23,6 @@ func MustParse(schema string) Type {
return t
}

func Parse(schema string) (Type, error) {
r := strings.NewReader(schema)
p := &parser{s: newScanner(r)}
return p.Parse()
}

type parser struct {
s *scanner
buf struct {
Expand Down
29 changes: 29 additions & 0 deletions type.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package jstn

import (
"encoding/json"
)

// A Kind represents a primitive JSON type.
type Kind int

Expand All @@ -18,3 +22,28 @@ type Type struct {
Properties map[string]*Type // Only for Objects
Items *Type // Only for Arrays
}

func (t Type) String() string {
out, _ := Generate(t)
return string(out)
}

// MarshalJSON implements json.Unmarshaler by converting t to its concise JSTN
// text representation.
func (t Type) MarshalJSON() ([]byte, error) {
out, _ := Generate(t)
return json.Marshal(string(out))
}

// UnmarshalJSON implements json.Unmarshaler by parsing a JSTN text string.
func (t *Type) UnmarshalJSON(b []byte) error {

var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}

tt, err := Parse(s)
*t = tt
return err
}
Loading

0 comments on commit 9b04954

Please sign in to comment.