lacerda 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.markdown +8 -0
- data/Gemfile +2 -0
- data/Guardfile +41 -0
- data/README.md +65 -0
- data/Rakefile +9 -0
- data/circle.yml +3 -0
- data/lacerda.gemspec +35 -0
- data/lib/lacerda.rb +10 -0
- data/lib/lacerda/compare/json_schema.rb +123 -0
- data/lib/lacerda/consume_contract.rb +18 -0
- data/lib/lacerda/consumed_object.rb +16 -0
- data/lib/lacerda/contract.rb +34 -0
- data/lib/lacerda/conversion.rb +132 -0
- data/lib/lacerda/conversion/apiary_to_json_schema.rb +9 -0
- data/lib/lacerda/conversion/data_structure.rb +93 -0
- data/lib/lacerda/conversion/error.rb +7 -0
- data/lib/lacerda/infrastructure.rb +74 -0
- data/lib/lacerda/object_description.rb +34 -0
- data/lib/lacerda/publish_contract.rb +22 -0
- data/lib/lacerda/published_object.rb +14 -0
- data/lib/lacerda/service.rb +63 -0
- data/lib/lacerda/tasks.rb +66 -0
- data/lib/lacerda/version.rb +3 -0
- data/stuff/contracts/author/consume.mson +0 -0
- data/stuff/contracts/author/publish.mson +20 -0
- data/stuff/contracts/edward/consume.mson +7 -0
- data/stuff/contracts/edward/publish.mson +0 -0
- data/stuff/explore/blueprint.apib +239 -0
- data/stuff/explore/swagger.yaml +139 -0
- data/stuff/explore/test.apib +14 -0
- data/stuff/explore/test.mson +239 -0
- data/stuff/explore/tmp.txt +27 -0
- metadata +250 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b9e353a73a35769ada04cc9efa1a32dfbedc1fee
|
4
|
+
data.tar.gz: a18205aa09824893c34a3c861dff1995f92fd28a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 605e8d8df037122645b4a113180ab043cb7472ca35792696e570c9398510b2c4f2703600cce3c3fdceee6f3fe473bbba44c1d259b7dfe9842cac85b31cb4ab0b
|
7
|
+
data.tar.gz: 4e9ad371ccb84ba3667c973011d216d0bddae5454a85a4aadfc638053d71c83ddb537cd1ae300561384b6e51c94e2929359c583dac46618b55d2a253f1f6b367
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
minimum-term
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2
|
data/CHANGELOG.markdown
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
guard :bundler do
|
19
|
+
watch('Gemfile')
|
20
|
+
# Uncomment next line if your Gemfile contains the `gemspec' command.
|
21
|
+
watch(/^.+\.gemspec/)
|
22
|
+
end
|
23
|
+
|
24
|
+
guard 'ctags-bundler', :src_path => ["lib"] do
|
25
|
+
watch(/^(app|lib|spec\/support)\/.*\.rb$/)
|
26
|
+
watch('Gemfile.lock')
|
27
|
+
watch(/^.+\.gemspec/)
|
28
|
+
end
|
29
|
+
|
30
|
+
guard :rspec, cmd: 'IGNORE_LOW_COVERAGE=1 rspec', all_on_start: true do
|
31
|
+
watch(%r{^spec/support/.*\.mson$}) { "spec" }
|
32
|
+
watch(%r{^spec/support/.*\.rb$}) { "spec" }
|
33
|
+
watch('spec/spec_helper.rb') { "spec" }
|
34
|
+
|
35
|
+
# We could run individual specs, sure, but for now I dictate the tests
|
36
|
+
# are only green when we have 100% coverage, so partial runs will never
|
37
|
+
# succeed. Therefore, always run all the things.
|
38
|
+
watch(%r{^(spec/.+_spec\.rb)$}) { "spec" }
|
39
|
+
watch(%r{^lib/(.+)\.rb$}) { "spec" }
|
40
|
+
end
|
41
|
+
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Lacerda [![Circle CI](https://circleci.com/gh/moviepilot/minimum-term/tree/master.svg?style=svg)](https://circleci.com/gh/moviepilot/minimum-term/tree/master) [![Coverage Status](https://coveralls.io/repos/moviepilot/minimum-term/badge.svg?branch=master&service=github)](https://coveralls.io/github/moviepilot/minimum-term?branch=master) [![Code Climate](https://codeclimate.com/github/moviepilot/minimum-term/badges/gpa.svg)](https://codeclimate.com/github/moviepilot/minimum-term) [![Dependency Status](https://gemnasium.com/moviepilot/minimum-term.svg)](https://gemnasium.com/moviepilot/minimum-term)
|
2
|
+
|
3
|
+
![](https://dl.dropboxusercontent.com/u/1953503/lacerda.jpg)
|
4
|
+
> «We need total coverage»<sup>[1](http://www.dailyscript.com/scripts/fearandloathing.html)</sup>
|
5
|
+
|
6
|
+
This gem can:
|
7
|
+
|
8
|
+
- convert MSON to JSON Schema files
|
9
|
+
- read a directory which contains one directory per service
|
10
|
+
- read a publish.mson and a consume.mson from each service
|
11
|
+
- build a model of your infrastructure knowing
|
12
|
+
- which services publishes what to which other service
|
13
|
+
- which service consumes what from which other service
|
14
|
+
- if all services consume and publish conforming to their contracts.
|
15
|
+
|
16
|
+
You likely don't want to use it on its own but head on over to the [Zeta](https://github.com/moviepilot/zeta) gem which explains things in more detail. If you're just looking for ONE way to transform MSON files into JSON Schema, read on:
|
17
|
+
|
18
|
+
## Getting started
|
19
|
+
First, check out [this API Blueprint map](https://github.com/apiaryio/api-blueprint/wiki/API-Blueprint-Map) to understand how _API Blueprint_ documents are laid out:
|
20
|
+
|
21
|
+
![API Blueprint map](https://raw.githubusercontent.com/apiaryio/api-blueprint/master/assets/map.png)
|
22
|
+
|
23
|
+
You can see that their structure covers a full API use case with resource groups, single resources, actions on those resources including requests and responses. All we want, though, is the little red top level branch called `Data structures`.
|
24
|
+
|
25
|
+
We're using a ruby gem called [RedSnow](https://github.com/apiaryio/redsnow), which has bindings to [SnowCrash](https://github.com/apiaryio/snowcrash) which parses _API Blueprints_ into an AST.
|
26
|
+
|
27
|
+
Luckily, a rake task does all that for you. To convert all `*.mson` files in `contracts/` into `*.schema.json` files,
|
28
|
+
|
29
|
+
put this in your `Rakefile`:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
require "lacerda/tasks"
|
33
|
+
```
|
34
|
+
|
35
|
+
and smoke it:
|
36
|
+
|
37
|
+
```shell
|
38
|
+
/home/dev/lacerda$ DATA_DIR=contracts/ rake lacerda:mson_to_json_schema
|
39
|
+
Converting 4 files:
|
40
|
+
OK /home/dev/lacerda/contracts/consumer/consume.mson
|
41
|
+
OK /home/dev/lacerda/contracts/invalid_property/consume.mson
|
42
|
+
OK /home/dev/lacerda/contracts/missing_required/consume.mson
|
43
|
+
OK /home/dev/lacerda/contracts/publisher/publish.mson
|
44
|
+
/home/dev/lacerda$
|
45
|
+
```
|
46
|
+
|
47
|
+
## Tests and development
|
48
|
+
- run `bundle` once
|
49
|
+
- run `guard` in a spare terminal which will run the tests,
|
50
|
+
install gems, and so forth
|
51
|
+
- run `rspec spec` to run all the tests
|
52
|
+
- check out `open coverage/index.html` or `open coverage/rcov/index.html`
|
53
|
+
- run `bundle console` to play around with a console
|
54
|
+
|
55
|
+
## Structure
|
56
|
+
|
57
|
+
By converting all files in a directory this gem will build up the following relationships:
|
58
|
+
|
59
|
+
- Infrastructure
|
60
|
+
- Service
|
61
|
+
- Contracts
|
62
|
+
- Publish contract
|
63
|
+
- PublishedObjects
|
64
|
+
- Consume contract
|
65
|
+
- ConsumedObjects
|
data/Rakefile
ADDED
data/circle.yml
ADDED
data/lacerda.gemspec
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'lacerda/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "lacerda"
|
8
|
+
spec.version = Lacerda::VERSION
|
9
|
+
spec.authors = ["Jannis Hermanns"]
|
10
|
+
spec.email = ["jannis@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = 'Markdown publish/consume contract parser and validator'
|
13
|
+
spec.description = 'Specify which objects your services publish or consume in MSON (markdown) and let this gem validate these contracts.'
|
14
|
+
spec.homepage = "https://github.com/moviepilot/lacerda"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "bin"
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
spec.license = 'MIT'
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "activesupport"
|
23
|
+
spec.add_runtime_dependency "rake", ["~> 10.2"]
|
24
|
+
spec.add_runtime_dependency "json-schema", ["~> 2.5"]
|
25
|
+
spec.add_runtime_dependency "redsnow", ["~> 0.4"]
|
26
|
+
spec.add_runtime_dependency "colorize"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", ["~> 1"]
|
29
|
+
spec.add_development_dependency "guard-bundler", ["~> 2.1"]
|
30
|
+
spec.add_development_dependency "guard-ctags-bundler", ["~> 1.4"]
|
31
|
+
spec.add_development_dependency "guard-rspec", ["~> 4.6"]
|
32
|
+
spec.add_development_dependency "rspec", ["~> 3.3"]
|
33
|
+
spec.add_development_dependency "coveralls", ["~> 0.8"]
|
34
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
35
|
+
end
|
data/lib/lacerda.rb
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
module Lacerda
|
2
|
+
module Compare
|
3
|
+
class JsonSchema
|
4
|
+
ERRORS = {
|
5
|
+
:ERR_ARRAY_ITEM_MISMATCH => nil,
|
6
|
+
:ERR_MISSING_DEFINITION => nil,
|
7
|
+
:ERR_MISSING_POINTER => nil,
|
8
|
+
:ERR_MISSING_PROPERTY => nil,
|
9
|
+
:ERR_MISSING_REQUIRED => nil,
|
10
|
+
:ERR_MISSING_TYPE_AND_REF => nil,
|
11
|
+
:ERR_TYPE_MISMATCH => nil,
|
12
|
+
:ERR_NOT_SUPPORTED => nil
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_reader :errors
|
16
|
+
|
17
|
+
def initialize(containing_schema)
|
18
|
+
@containing_schema = containing_schema
|
19
|
+
end
|
20
|
+
|
21
|
+
def contains?(contained_schema, pry = false)
|
22
|
+
@errors = []
|
23
|
+
@contained_schema = contained_schema
|
24
|
+
definitions_contained?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def definitions_contained?
|
30
|
+
@contained_schema['definitions'].each do |property, contained_property|
|
31
|
+
containing_property = @containing_schema['definitions'][property]
|
32
|
+
return _e(:ERR_MISSING_DEFINITION, [property]) unless containing_property
|
33
|
+
return false unless schema_contains?(containing_property, contained_property, [property])
|
34
|
+
end
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
def _e(error, location, extra = nil)
|
39
|
+
message = [ERRORS[error], extra].compact.join(": ")
|
40
|
+
@errors.push(error: error, message: message, location: location.join("/"))
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def schema_contains?(publish, consume, location = [])
|
45
|
+
|
46
|
+
# We can only compare types and $refs, so let's make
|
47
|
+
# sure they're there
|
48
|
+
return _e!(:ERR_MISSING_TYPE_AND_REF) unless
|
49
|
+
(consume['type'] or consume['$ref']) and
|
50
|
+
(publish['type'] or publish['$ref'])
|
51
|
+
|
52
|
+
# There's four possibilities here:
|
53
|
+
#
|
54
|
+
# 1) publish and consume have a type defined
|
55
|
+
# 2) publish and consume have a $ref defined
|
56
|
+
# 3) publish has a $ref defined, and consume an inline object
|
57
|
+
# 4) consume has a $ref defined, and publish an inline object
|
58
|
+
# (we don't support this yet, as otherwise couldn't check for
|
59
|
+
# missing definitions, because we could never know if something
|
60
|
+
# specified in the definitions of the consuming schema exists in
|
61
|
+
# the publishing schema as an inline property somewhere).
|
62
|
+
# TODO: check if what I just said makes sense. I'm not sure anymore.
|
63
|
+
# Let's go:
|
64
|
+
|
65
|
+
# 1)
|
66
|
+
if (consume['type'] and publish['type'])
|
67
|
+
if consume['type'] != publish['type']
|
68
|
+
return _e(:ERR_TYPE_MISMATCH, location, "#{consume['type']} != #{publish['type']}")
|
69
|
+
end
|
70
|
+
|
71
|
+
# 2)
|
72
|
+
elsif(consume['$ref'] and publish['$ref'])
|
73
|
+
resolved_consume = resolve_pointer(consume['$ref'], @contained_schema)
|
74
|
+
resolved_publish = resolve_pointer(publish['$ref'], @containing_schema)
|
75
|
+
return schema_contains?(resolved_publish, resolved_consume, location)
|
76
|
+
|
77
|
+
# 3)
|
78
|
+
elsif(consume['type'] and publish['$ref'])
|
79
|
+
if resolved_ref = resolve_pointer(publish['$ref'], @containing_schema)
|
80
|
+
return schema_contains?(resolved_ref, consume, location)
|
81
|
+
else
|
82
|
+
return _e(:ERR_MISSING_POINTER, location, publish['$ref'])
|
83
|
+
end
|
84
|
+
|
85
|
+
# 4)
|
86
|
+
elsif(consume['$ref'] and publish['type'])
|
87
|
+
return _e(:ERR_NOT_SUPPORTED, location)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Make sure required properties in consume are required in publish
|
91
|
+
consume_required = consume['required'] || []
|
92
|
+
publish_required = publish['required'] || []
|
93
|
+
missing = (consume_required - publish_required)
|
94
|
+
return _e(:ERR_MISSING_REQUIRED, location, missing.to_json) unless missing.empty?
|
95
|
+
|
96
|
+
# We already know that publish and consume's type are equal
|
97
|
+
# but if they're objects, we need to do some recursion
|
98
|
+
if consume['type'] == 'object'
|
99
|
+
consume['properties'].each do |property, schema|
|
100
|
+
return _e(:ERR_MISSING_PROPERTY, location, property) unless publish['properties'][property]
|
101
|
+
return false unless schema_contains?(publish['properties'][property], schema, location + [property])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
if consume['type'] == 'array'
|
106
|
+
sorted_publish = publish['items'].sort
|
107
|
+
consume['items'].sort.each_with_index do |item, i|
|
108
|
+
next if schema_contains?(sorted_publish[i], item)
|
109
|
+
return _e(:ERR_ARRAY_ITEM_MISMATCH, location)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
def resolve_pointer(pointer, schema)
|
117
|
+
type = pointer[/\#\/definitions\/([^\/]+)$/, 1]
|
118
|
+
return false unless type
|
119
|
+
schema['definitions'][type]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'lacerda/contract'
|
2
|
+
|
3
|
+
module Lacerda
|
4
|
+
class ConsumeContract < Lacerda::Contract
|
5
|
+
def object_description_class
|
6
|
+
Lacerda::ConsumedObject
|
7
|
+
end
|
8
|
+
|
9
|
+
def scoped_schema(service)
|
10
|
+
# Poor man's deep clone: json 🆗 🆒
|
11
|
+
filtered_schema = JSON.parse(schema.to_json)
|
12
|
+
filtered_schema['definitions'].select! do |k|
|
13
|
+
k.underscore.start_with?(service.name.underscore+Lacerda::SCOPE_SEPARATOR)
|
14
|
+
end
|
15
|
+
filtered_schema
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'lacerda/object_description'
|
2
|
+
|
3
|
+
module Lacerda
|
4
|
+
class ConsumedObject < Lacerda::ObjectDescription
|
5
|
+
|
6
|
+
def publisher
|
7
|
+
i = @scoped_name.index(Lacerda::SCOPE_SEPARATOR)
|
8
|
+
return @defined_in_service unless i
|
9
|
+
@defined_in_service.infrastructure.services[@scoped_name[0...i].underscore.to_sym]
|
10
|
+
end
|
11
|
+
|
12
|
+
def consumer
|
13
|
+
@defined_in_service
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
require 'lacerda/published_object'
|
3
|
+
require 'lacerda/consumed_object'
|
4
|
+
|
5
|
+
module Lacerda
|
6
|
+
class Contract
|
7
|
+
attr_reader :service, :schema
|
8
|
+
|
9
|
+
def initialize(service, schema_or_file)
|
10
|
+
@service = service
|
11
|
+
load_schema(schema_or_file)
|
12
|
+
end
|
13
|
+
|
14
|
+
def objects
|
15
|
+
return [] unless @schema[:definitions]
|
16
|
+
@schema[:definitions].map do |scoped_name, schema|
|
17
|
+
object_description_class.new(service, scoped_name, schema)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def load_schema(schema_or_file)
|
24
|
+
if schema_or_file.is_a?(Hash)
|
25
|
+
@schema = schema_or_file
|
26
|
+
elsif File.readable?(schema_or_file)
|
27
|
+
@schema = JSON.parse(open(schema_or_file).read)
|
28
|
+
else
|
29
|
+
@schema = {}
|
30
|
+
end
|
31
|
+
@schema = @schema.with_indifferent_access
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'open3'
|
3
|
+
require 'lacerda/conversion/apiary_to_json_schema'
|
4
|
+
require 'lacerda/conversion/error'
|
5
|
+
require 'redsnow'
|
6
|
+
|
7
|
+
module Lacerda
|
8
|
+
module Conversion
|
9
|
+
def self.mson_to_json_schema(filename:, keep_intermediary_files: false, verbose: false)
|
10
|
+
begin
|
11
|
+
mson_to_json_schema!(filename: filename, keep_intermediary_files: keep_intermediary_files, verbose: verbose)
|
12
|
+
puts "OK ".green + filename if verbose
|
13
|
+
true
|
14
|
+
rescue
|
15
|
+
puts "ERROR ".red + filename if verbose
|
16
|
+
false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.mson_to_json_schema!(filename:, keep_intermediary_files: false, verbose: true)
|
21
|
+
|
22
|
+
# For now, we'll use the containing directory's name as a scope
|
23
|
+
service_scope = File.dirname(filename).split(File::SEPARATOR).last.underscore
|
24
|
+
|
25
|
+
# Parse MSON to an apiary blueprint AST
|
26
|
+
# (see https://github.com/apiaryio/api-blueprint/wiki/API-Blueprint-Map)
|
27
|
+
ast_file = mson_to_ast_json(filename)
|
28
|
+
|
29
|
+
# Pluck out Data structures from it
|
30
|
+
data_structures = data_structures_from_blueprint_ast(ast_file)
|
31
|
+
|
32
|
+
# Generate json schema from each contained data structure
|
33
|
+
schema = {
|
34
|
+
"$schema" => "http://json-schema.org/draft-04/schema#",
|
35
|
+
"title" => service_scope,
|
36
|
+
"definitions" => {},
|
37
|
+
"type" => "object",
|
38
|
+
"properties" => {},
|
39
|
+
}
|
40
|
+
|
41
|
+
# The scope for the data structure is the name of the service
|
42
|
+
# publishing the object. So if we're parsing a 'consume' schema,
|
43
|
+
# the containing objects are alredy scoped (because a consume
|
44
|
+
# schema says 'i consume object X from service Y'.
|
45
|
+
basename = File.basename(filename)
|
46
|
+
if basename.end_with?("publish.mson")
|
47
|
+
data_structure_autoscope = service_scope
|
48
|
+
elsif basename.end_with?("consume.mson")
|
49
|
+
data_structure_autoscope = nil
|
50
|
+
else
|
51
|
+
raise Error, "Invalid filename #{basename}, can't tell if it's a publish or consume schema"
|
52
|
+
end
|
53
|
+
|
54
|
+
# The json schema we're constructing contains every known
|
55
|
+
# object type in the 'definitions'. So if we have definitions for
|
56
|
+
# the objects User, Post and Tag, the schema will look like this:
|
57
|
+
#
|
58
|
+
# {
|
59
|
+
# "$schema": "..."
|
60
|
+
#
|
61
|
+
# "definitions": {
|
62
|
+
# "user": { "type": "object", "properties": { ... }}
|
63
|
+
# "post": { "type": "object", "properties": { ... }}
|
64
|
+
# "tag": { "type": "object", "properties": { ... }}
|
65
|
+
# }
|
66
|
+
#
|
67
|
+
# "properties": {
|
68
|
+
# "user": "#/definitions/user"
|
69
|
+
# "post": "#/definitions/post"
|
70
|
+
# "tag": "#/definitions/tag"
|
71
|
+
# }
|
72
|
+
#
|
73
|
+
# }
|
74
|
+
#
|
75
|
+
# So when testing an object of type `user` against this schema,
|
76
|
+
# we need to wrap it as:
|
77
|
+
#
|
78
|
+
# {
|
79
|
+
# user: {
|
80
|
+
# "your": "actual",
|
81
|
+
# "data": "goes here"
|
82
|
+
# }
|
83
|
+
# }
|
84
|
+
#
|
85
|
+
data_structures.each do |data|
|
86
|
+
id = data['name']['literal']
|
87
|
+
json= DataStructure.new(id, data, data_structure_autoscope).to_json
|
88
|
+
member = json.delete('title')
|
89
|
+
schema['definitions'][member] = json
|
90
|
+
schema['properties'][member] = {"$ref" => "#/definitions/#{member}"}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Write it in a file
|
94
|
+
outfile = filename.gsub(/\.\w+$/, '.schema.json')
|
95
|
+
File.open(outfile, 'w'){ |f| f.puts JSON.pretty_generate(schema) }
|
96
|
+
|
97
|
+
# Clean up
|
98
|
+
FileUtils.rm_f(ast_file) unless keep_intermediary_files
|
99
|
+
true
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.data_structures_from_blueprint_ast(filename)
|
103
|
+
c = JSON.parse(open(filename).read)['ast']['content'].first
|
104
|
+
return [] unless c
|
105
|
+
c['content']
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.mson_to_ast_json(filename)
|
109
|
+
input = filename
|
110
|
+
output = filename.gsub(/\.\w+$/, '.blueprint-ast.json')
|
111
|
+
|
112
|
+
|
113
|
+
parse_result = FFI::MemoryPointer.new :pointer
|
114
|
+
RedSnow::Binding.drafter_c_parse(open(input).read, 0, parse_result)
|
115
|
+
parse_result = parse_result.get_pointer(0)
|
116
|
+
|
117
|
+
status = -1
|
118
|
+
result = ''
|
119
|
+
|
120
|
+
unless parse_result.null?
|
121
|
+
status = 0
|
122
|
+
result = parse_result.read_string
|
123
|
+
end
|
124
|
+
|
125
|
+
File.open(output, 'w'){ |f| f.puts(result) }
|
126
|
+
|
127
|
+
output
|
128
|
+
ensure
|
129
|
+
RedSnow::Memory.free(parse_result)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|