prmd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +19 -0
- data/.travis.yml +2 -0
- data/CONTRIBUTORS.md +6 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.md +20 -0
- data/README.md +72 -0
- data/Rakefile +8 -0
- data/bin/prmd +104 -0
- data/docs/schemata.md +162 -0
- data/lib/prmd.rb +15 -0
- data/lib/prmd/commands/combine.rb +5 -0
- data/lib/prmd/commands/doc.rb +110 -0
- data/lib/prmd/commands/expand.rb +113 -0
- data/lib/prmd/commands/init.rb +101 -0
- data/lib/prmd/commands/verify.rb +66 -0
- data/lib/prmd/schema.rb +107 -0
- data/lib/prmd/version.rb +3 -0
- data/lib/prmd/views/endpoint.erb +107 -0
- data/lib/prmd/views/parameters.erb +16 -0
- data/prmd.gemspec +27 -0
- data/test/helpers.rb +10 -0
- data/test/schema_test.rb +13 -0
- data/test/schemas/input/user.json +102 -0
- metadata +143 -0
data/lib/prmd.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "diff-lcs"
|
2
|
+
require "erubis"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
dir = File.dirname(__FILE__)
|
6
|
+
require File.join(dir, 'prmd', 'commands', 'combine')
|
7
|
+
require File.join(dir, 'prmd', 'commands', 'doc')
|
8
|
+
require File.join(dir, 'prmd', 'commands', 'expand')
|
9
|
+
require File.join(dir, 'prmd', 'commands', 'init')
|
10
|
+
require File.join(dir, 'prmd', 'commands', 'verify')
|
11
|
+
require File.join(dir, 'prmd', 'schema')
|
12
|
+
require File.join(dir, 'prmd', 'version')
|
13
|
+
|
14
|
+
module Prmd
|
15
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
def extract_attributes(schema, properties)
|
2
|
+
attributes = []
|
3
|
+
properties.each do |key, value|
|
4
|
+
# found a reference to another element:
|
5
|
+
value = schema.dereference(value)
|
6
|
+
if value.has_key?('anyOf')
|
7
|
+
descriptions = []
|
8
|
+
examples = []
|
9
|
+
|
10
|
+
# sort anyOf! always show unique identifier first
|
11
|
+
anyof = value['anyOf'].sort_by do |property|
|
12
|
+
property['$ref'].split('/').last.gsub('id', 'a')
|
13
|
+
end
|
14
|
+
|
15
|
+
anyof.each do |ref|
|
16
|
+
nested_field = schema.dereference(ref)
|
17
|
+
descriptions << nested_field['description']
|
18
|
+
examples << nested_field['example']
|
19
|
+
end
|
20
|
+
|
21
|
+
# avoid repetition :}
|
22
|
+
if descriptions.size > 1
|
23
|
+
descriptions.first.gsub!(/ of (this )?.*/, "")
|
24
|
+
descriptions[1..-1].map { |d| d.gsub!(/unique /, "") }
|
25
|
+
end
|
26
|
+
description = descriptions.join(" or ")
|
27
|
+
example = doc_example(*examples)
|
28
|
+
attributes << [key, "string", description, example]
|
29
|
+
|
30
|
+
# found a nested object
|
31
|
+
elsif value['type'] == ['object'] && value['properties']
|
32
|
+
nested = extract_attributes(schema, value['properties'])
|
33
|
+
nested.each do |attribute|
|
34
|
+
attribute[0] = "#{key}:#{attribute[0]}"
|
35
|
+
end
|
36
|
+
attributes.concat(nested)
|
37
|
+
# just a regular attribute
|
38
|
+
else
|
39
|
+
description = value['description']
|
40
|
+
if value['enum']
|
41
|
+
description += '<br/><b>one of:</b>' + doc_example(*value['enum'])
|
42
|
+
end
|
43
|
+
example = doc_example(value['example'])
|
44
|
+
attributes << [key, doc_type(value), description, example]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
return attributes
|
48
|
+
end
|
49
|
+
|
50
|
+
def doc_type(property)
|
51
|
+
schema_type = property["type"].dup
|
52
|
+
type = "nullable " if schema_type.delete("null")
|
53
|
+
type.to_s + (property["format"] || schema_type.first)
|
54
|
+
end
|
55
|
+
|
56
|
+
def doc_example(*examples)
|
57
|
+
examples.map { |e| "<code>#{e.to_json}</code>" }.join(" or ")
|
58
|
+
end
|
59
|
+
|
60
|
+
module Prmd
|
61
|
+
def self.doc(schema, options={})
|
62
|
+
root_url = schema['links'].find{|l| l['rel'] == 'root'}['href'] rescue schema['url']
|
63
|
+
|
64
|
+
doc = (options[:prepend] || []).map do |path|
|
65
|
+
File.open(path, 'r').read + "\n"
|
66
|
+
end
|
67
|
+
|
68
|
+
doc << schema['definitions'].map do |_, definition|
|
69
|
+
next if (definition['links'] || []).empty?
|
70
|
+
resource = definition['id'].split('/').last
|
71
|
+
serialization = {}
|
72
|
+
if definition['definitions'].has_key?('identity')
|
73
|
+
identifiers = if definition['definitions']['identity'].has_key?('anyOf')
|
74
|
+
definition['definitions']['identity']['anyOf']
|
75
|
+
else
|
76
|
+
[definitions['definitions']['identity']]
|
77
|
+
end
|
78
|
+
|
79
|
+
identifiers = identifiers.map {|ref| ref['$ref'].split('/').last }
|
80
|
+
end
|
81
|
+
if definition['properties']
|
82
|
+
definition['properties'].each do |key, value|
|
83
|
+
unless value.has_key?('properties')
|
84
|
+
serialization[key] = schema.dereference(value)['example']
|
85
|
+
else
|
86
|
+
serialization[key] = {}
|
87
|
+
value['properties'].each do |k,v|
|
88
|
+
serialization[key][k] = schema.dereference(v)['example']
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
else
|
93
|
+
serialization.merge!(definition['example'])
|
94
|
+
end
|
95
|
+
|
96
|
+
title = definition['title'].split(' - ', 2).last
|
97
|
+
|
98
|
+
Erubis::Eruby.new(File.read(File.dirname(__FILE__) + "/../views/endpoint.erb")).result({
|
99
|
+
definition: definition,
|
100
|
+
identifiers: identifiers,
|
101
|
+
resource: resource,
|
102
|
+
root_url: root_url,
|
103
|
+
schema: schema,
|
104
|
+
serialization: serialization,
|
105
|
+
title: title,
|
106
|
+
params_template: File.read(File.dirname(__FILE__) + "/../views/parameters.erb"),
|
107
|
+
}) + "\n"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
original = {
|
2
|
+
'description' => 'A foo is bar.',
|
3
|
+
'id' => 'schema/foo',
|
4
|
+
'title' => 'API - Foo',
|
5
|
+
|
6
|
+
'definitions' => {
|
7
|
+
'created-at' => {
|
8
|
+
'description' => 'when foo was created',
|
9
|
+
'format' => 'date-time'
|
10
|
+
},
|
11
|
+
'email' => {
|
12
|
+
'description' => 'email for foo',
|
13
|
+
'format' => 'email'
|
14
|
+
},
|
15
|
+
'id' => {
|
16
|
+
'description' => 'unique identifier for foo',
|
17
|
+
'format' => 'uuid'
|
18
|
+
},
|
19
|
+
'updated-at' => {
|
20
|
+
'description' => 'when foo was created',
|
21
|
+
'format' => 'date-time',
|
22
|
+
'type' => ['null', 'string']
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
26
|
+
expanded = original.dup
|
27
|
+
|
28
|
+
expanded = {
|
29
|
+
'$schema' => 'http://json-schema.org/draft-04/hyper-schema',
|
30
|
+
'definitions' => {},
|
31
|
+
'links' => [],
|
32
|
+
'properties' => {},
|
33
|
+
'type' => ['object']
|
34
|
+
}.merge!(expanded)
|
35
|
+
|
36
|
+
if original['definitions']
|
37
|
+
original['definitions'].each do |key, value|
|
38
|
+
default = case value['format']
|
39
|
+
when 'uuid'
|
40
|
+
{
|
41
|
+
'example' => '01234567-89ab-cdef-0123-456789abcdef',
|
42
|
+
'readOnly' => true,
|
43
|
+
'type' => ['string']
|
44
|
+
}
|
45
|
+
when 'email'
|
46
|
+
{
|
47
|
+
'example' => 'username@example.com',
|
48
|
+
'readOnly' => false,
|
49
|
+
'type' => ['string']
|
50
|
+
}
|
51
|
+
when 'date-time'
|
52
|
+
{
|
53
|
+
'example' => '2012-01-01T12:00:00Z',
|
54
|
+
'readOnly' => true,
|
55
|
+
'type' => ['string']
|
56
|
+
}
|
57
|
+
else
|
58
|
+
{
|
59
|
+
'readOnly' => false
|
60
|
+
}
|
61
|
+
end
|
62
|
+
expanded['definitions'][key] = default.merge!(value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if original['links']
|
67
|
+
original['links'].each do |key, value|
|
68
|
+
default = case value['title']
|
69
|
+
when 'Create'
|
70
|
+
{
|
71
|
+
'method' => 'POST',
|
72
|
+
'rel' => 'create'
|
73
|
+
}
|
74
|
+
when 'Delete'
|
75
|
+
{
|
76
|
+
'method' => 'DELETE',
|
77
|
+
'rel' => 'delete'
|
78
|
+
}
|
79
|
+
when 'Info'
|
80
|
+
{
|
81
|
+
'method' => 'GET',
|
82
|
+
'rel' => 'self'
|
83
|
+
}
|
84
|
+
when 'List'
|
85
|
+
{
|
86
|
+
'method' => 'GET',
|
87
|
+
'rel' => 'instances'
|
88
|
+
}
|
89
|
+
when 'Update'
|
90
|
+
{
|
91
|
+
'method' => 'PATCH',
|
92
|
+
'rel' => 'update'
|
93
|
+
}
|
94
|
+
end
|
95
|
+
expanded = original['links'][key] = default.merge!(value)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
#require 'json'
|
101
|
+
#require 'pp'
|
102
|
+
|
103
|
+
#puts
|
104
|
+
|
105
|
+
#puts original_json = JSON.pretty_generate(original)
|
106
|
+
#puts original_json.split("\n").length
|
107
|
+
|
108
|
+
#puts
|
109
|
+
|
110
|
+
#puts expanded_json = JSON.pretty_generate(expanded)
|
111
|
+
#puts expanded_json.split("\n").length
|
112
|
+
|
113
|
+
#puts
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Prmd
|
2
|
+
def self.init(resource, options={})
|
3
|
+
data = {
|
4
|
+
'$schema' => 'http://json-schema.org/draft-04/hyper-schema',
|
5
|
+
'title' => 'FIXME',
|
6
|
+
'type' => ['object'],
|
7
|
+
'definitions' => {},
|
8
|
+
'links' => [],
|
9
|
+
'properties' => {}
|
10
|
+
}
|
11
|
+
|
12
|
+
if options[:meta] && File.exists?(options[:meta])
|
13
|
+
data.merge!(JSON.parse(File.read(options[:meta])))
|
14
|
+
end
|
15
|
+
|
16
|
+
schema = Prmd::Schema.new(data)
|
17
|
+
|
18
|
+
if resource
|
19
|
+
schema['id'] = "schema/#{resource}"
|
20
|
+
schema['title'] = "#{schema['title']} - #{resource[0...1].upcase}#{resource[1..-1]}"
|
21
|
+
schema['definitions'] = {
|
22
|
+
"created_at" => {
|
23
|
+
"description" => "when #{resource} was created",
|
24
|
+
"example" => "2012-01-01T12:00:00Z",
|
25
|
+
"format" => "date-time",
|
26
|
+
"readOnly" => true,
|
27
|
+
"type" => ["string"]
|
28
|
+
},
|
29
|
+
"id" => {
|
30
|
+
"description" => "unique identifier of #{resource}",
|
31
|
+
"example" => "01234567-89ab-cdef-0123-456789abcdef",
|
32
|
+
"format" => "uuid",
|
33
|
+
"readOnly" => true,
|
34
|
+
"type" => ["string"]
|
35
|
+
},
|
36
|
+
"identity" => {
|
37
|
+
"$ref" => "/schema/#{resource}#/definitions/id"
|
38
|
+
},
|
39
|
+
"updated_at" => {
|
40
|
+
"description" => "when #{resource} was updated",
|
41
|
+
"example" => "2012-01-01T12:00:00Z",
|
42
|
+
"format" => "date-time",
|
43
|
+
"readOnly" => true,
|
44
|
+
"type" => ["string"]
|
45
|
+
}
|
46
|
+
}
|
47
|
+
schema['links'] = [
|
48
|
+
{
|
49
|
+
"description" => "Create a new #{resource}.",
|
50
|
+
"href" => "/#{resource}s",
|
51
|
+
"method" => "POST",
|
52
|
+
"rel" => "create",
|
53
|
+
"schema" => {
|
54
|
+
"properties" => {},
|
55
|
+
"type" => ["object"]
|
56
|
+
},
|
57
|
+
"title" => "Create"
|
58
|
+
},
|
59
|
+
{
|
60
|
+
"description" => "Delete an existing #{resource}.",
|
61
|
+
"href" => "/#{resource}s/{(%2Fschema%2F#{resource}%23%2Fdefinitions%2Fidentity)}",
|
62
|
+
"method" => "DELETE",
|
63
|
+
"rel" => "destroy",
|
64
|
+
"title" => "Delete"
|
65
|
+
},
|
66
|
+
{
|
67
|
+
"description" => "Info for existing #{resource}.",
|
68
|
+
"href" => "/#{resource}s/{(%2Fschema%2F#{resource}%23%2Fdefinitions%2Fidentity)}",
|
69
|
+
"method" => "GET",
|
70
|
+
"rel" => "self",
|
71
|
+
"title" => "Info"
|
72
|
+
},
|
73
|
+
{
|
74
|
+
"description" => "List existing #{resource}.",
|
75
|
+
"href" => "/#{resource}s",
|
76
|
+
"method" => "GET",
|
77
|
+
"rel" => "instances",
|
78
|
+
"title" => "List"
|
79
|
+
},
|
80
|
+
{
|
81
|
+
"description" => "Update an existing #{resource}.",
|
82
|
+
"href" => "/#{resource}s/{(%2Fschema%2F#{resource}%23%2Fdefinitions%2Fidentity)}",
|
83
|
+
"method" => "PATCH",
|
84
|
+
"rel" => "update",
|
85
|
+
"schema" => {
|
86
|
+
"properties" => {},
|
87
|
+
"type" => ["object"]
|
88
|
+
},
|
89
|
+
"title" => "Update"
|
90
|
+
}
|
91
|
+
]
|
92
|
+
schema['properties'] = {
|
93
|
+
"created_at" => { "$ref" => "/schema/#{resource}#/definitions/created_at" },
|
94
|
+
"id" => { "$ref" => "/schema/#{resource}#/definitions/id" },
|
95
|
+
"updated_at" => { "$ref" => "/schema/#{resource}#/definitions/updated_at" }
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
schema.to_s
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Prmd
|
2
|
+
def self.verify(schema)
|
3
|
+
errors = []
|
4
|
+
|
5
|
+
id = schema['id']
|
6
|
+
|
7
|
+
missing_requirements = []
|
8
|
+
%w{description id $schema title type definitions links properties}.each do |requirement|
|
9
|
+
unless schema.has_key?(requirement)
|
10
|
+
missing_requirements << requirement
|
11
|
+
end
|
12
|
+
end
|
13
|
+
missing_requirements.each do |missing_requirement|
|
14
|
+
errors << "Missing `#{id}#/#{missing_requirement}`"
|
15
|
+
end
|
16
|
+
|
17
|
+
if schema['definitions']
|
18
|
+
unless schema['definitions'].has_key?('identity')
|
19
|
+
errors << "Missing `#{id}#/definitions/identity`"
|
20
|
+
end
|
21
|
+
schema['definitions'].each do |key, value|
|
22
|
+
missing_requirements = []
|
23
|
+
unless key == 'identity'
|
24
|
+
%w{description readOnly type}.each do |requirement|
|
25
|
+
unless schema['definitions'][key].has_key?(requirement)
|
26
|
+
missing_requirements << requirement
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
# check for example, unless they are nested in array/object
|
31
|
+
type = schema['definitions'][key]['type']
|
32
|
+
unless type.nil? || type.include?('array') || type.include?('object')
|
33
|
+
unless schema['definitions'][key].has_key?('example')
|
34
|
+
missing_requirements << 'example'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
missing_requirements.each do |missing_requirement|
|
38
|
+
errors << "Missing `#{id}#/definitions/#{key}/#{missing_requirement}`"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
if schema['links']
|
44
|
+
schema['links'].each do |link|
|
45
|
+
missing_requirements = []
|
46
|
+
%w{description href method rel title}.each do |requirement|
|
47
|
+
unless link.has_key?(requirement)
|
48
|
+
missing_requirements << requirement
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if link.has_key?('schema')
|
52
|
+
%w{properties type}.each do |requirement|
|
53
|
+
unless link['schema'].has_key?(requirement)
|
54
|
+
missing_requirements << "schema/#{requirement}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
missing_requirements.each do |missing_requirement|
|
59
|
+
errors << "Missing #{missing_requirement} in `#{link}` link for `#{id}`"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
errors
|
65
|
+
end
|
66
|
+
end
|
data/lib/prmd/schema.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
module Prmd
|
2
|
+
class Schema
|
3
|
+
|
4
|
+
def [](key)
|
5
|
+
@data[key]
|
6
|
+
end
|
7
|
+
|
8
|
+
def []=(key, value)
|
9
|
+
@data[key] = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.load(path, options={})
|
13
|
+
unless File.directory?(path)
|
14
|
+
data = JSON.parse(File.read(path))
|
15
|
+
else
|
16
|
+
data = {
|
17
|
+
'$schema' => 'http://json-schema.org/draft-04/hyper-schema',
|
18
|
+
'definitions' => {},
|
19
|
+
'properties' => {},
|
20
|
+
'type' => ['object']
|
21
|
+
}
|
22
|
+
|
23
|
+
if options[:meta] && File.exists?(options[:meta])
|
24
|
+
data.merge!(JSON.parse(File.read(options[:meta])))
|
25
|
+
end
|
26
|
+
|
27
|
+
Dir.glob(File.join(path, '**', '*.json')).each do |schema|
|
28
|
+
schema_data = JSON.parse(File.read(schema))
|
29
|
+
id = if schema_data['id']
|
30
|
+
schema_data['id'].gsub('schema/', '')
|
31
|
+
end
|
32
|
+
next if id.nil? || id[0..0] == '_' # FIXME: remove this exception?
|
33
|
+
|
34
|
+
data['definitions'][id] = schema_data
|
35
|
+
reference_localizer = lambda do |datum|
|
36
|
+
case datum
|
37
|
+
when Array
|
38
|
+
datum.map {|element| reference_localizer.call(element)}
|
39
|
+
when Hash
|
40
|
+
if datum.has_key?('$ref')
|
41
|
+
datum['$ref'] = datum['$ref'].gsub(%r{/schema/([^#]*)#}, '#/definitions/\1')
|
42
|
+
end
|
43
|
+
if datum.has_key?('href')
|
44
|
+
datum['href'] = datum['href'].gsub(%r{%2Fschema%2F([^%]*)%23%2F}, '%23%2Fdefinitions%2F\1%2F')
|
45
|
+
end
|
46
|
+
datum.each { |k,v| datum[k] = reference_localizer.call(v) }
|
47
|
+
else
|
48
|
+
datum
|
49
|
+
end
|
50
|
+
end
|
51
|
+
reference_localizer.call(data['definitions'][id])
|
52
|
+
|
53
|
+
data['properties'][id] = { '$ref' => "#/definitions/#{id}" }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
self.new(data)
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(new_data = {})
|
61
|
+
convert_type_to_array = lambda do |datum|
|
62
|
+
case datum
|
63
|
+
when Array
|
64
|
+
datum.map { |element| convert_type_to_array.call(element) }
|
65
|
+
when Hash
|
66
|
+
if datum.has_key?('type')
|
67
|
+
datum['type'] = [*datum['type']]
|
68
|
+
end
|
69
|
+
datum.each { |k,v| datum[k] = convert_type_to_array.call(v) }
|
70
|
+
else
|
71
|
+
datum
|
72
|
+
end
|
73
|
+
end
|
74
|
+
@data = convert_type_to_array.call(new_data)
|
75
|
+
end
|
76
|
+
|
77
|
+
def dereference(reference)
|
78
|
+
if reference.is_a?(Hash)
|
79
|
+
if reference.has_key?('$ref')
|
80
|
+
key = reference['$ref']
|
81
|
+
else
|
82
|
+
return reference # no dereference needed
|
83
|
+
end
|
84
|
+
else
|
85
|
+
key = reference
|
86
|
+
end
|
87
|
+
begin
|
88
|
+
datum = @data
|
89
|
+
key.gsub(%r{[^#]*#/}, '').split('/').each do |fragment|
|
90
|
+
datum = datum[fragment]
|
91
|
+
end
|
92
|
+
datum
|
93
|
+
rescue => error
|
94
|
+
$stderr.puts("Failed to dereference `#{key}`")
|
95
|
+
raise(error)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_s
|
100
|
+
new_json = JSON.pretty_generate(@data)
|
101
|
+
# nuke empty lines
|
102
|
+
new_json = new_json.split("\n").delete_if {|line| line.empty?}.join("\n") + "\n"
|
103
|
+
new_json
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|