prmd 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/Gemfile.lock +3 -1
- data/Rakefile +1 -1
- data/bin/prmd +6 -3
- data/docs/schemata.md +4 -2
- data/lib/prmd.rb +1 -0
- data/lib/prmd/commands/combine.rb +12 -2
- data/lib/prmd/commands/render.rb +9 -1
- data/lib/prmd/commands/verify.rb +39 -75
- data/lib/prmd/schema.rb +8 -4
- data/lib/prmd/template.rb +19 -0
- data/lib/prmd/templates/schema.erb +13 -8
- data/lib/prmd/templates/schemata.erb +17 -146
- data/lib/prmd/templates/schemata/helper.erb +80 -0
- data/lib/prmd/templates/schemata/link.erb +58 -0
- data/lib/prmd/templates/schemata/link_curl_example.erb +25 -0
- data/lib/prmd/version.rb +1 -1
- data/prmd.gemspec +1 -0
- data/schemas/hyper-schema.json +168 -0
- data/schemas/interagent-hyper-schema.json +175 -0
- data/schemas/schema.json +150 -0
- data/test/commands/verify_test.rb +187 -0
- data/test/helpers.rb +1 -0
- metadata +25 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YTBiMDJmODY4NDAwMTE1ZTMzZTJiY2I2MzA1M2VkMzM2OWZkYmI5NA==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MTc2MjY0MjgzMDEwYzhhYWVmYzZkY2VhNjI0YmM3ZmI4MThhYzFlZA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZDJkMzVkZTk4ZTI1YmU5M2Q2MGVmNzZhZjI0YzFmODYwNDE0YThlYjA1MTYw
|
10
|
+
NDI0ZDNhYmFjOTczNTllOTBjZDgyNDYwZGQyMzZhYmVlMzZlMTFjZTIzYWVm
|
11
|
+
MmNjOTA0MTIzMWRlNjNmMDU5YTQ3YjZkZjQyZGQ1MzY1YWNlNGI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NmI3N2ViOGIyZDE1NTljODExNDhkY2E2MDlmZThhZTFiZWE2YmFhNmQyNTg0
|
14
|
+
MTBjYzczMjg4NDI1OGVjODlkOTI5NzU5YjllYmMyZGJjYzQ2N2I1YWZlMGM2
|
15
|
+
MzY3YzIwM2RjYTlkNzk5MzExNjU0NjAxZDUzYTIzODlkZDQ2YjY=
|
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
data/bin/prmd
CHANGED
@@ -16,6 +16,9 @@ commands = {
|
|
16
16
|
opts.on("-p", "--prepend header,overview", Array, "Prepend files to output") do |p|
|
17
17
|
options[:prepend] = p
|
18
18
|
end
|
19
|
+
opts.on("-c", "--content-type application/json", String, "Content-Type header") do |c|
|
20
|
+
options[:content_type] = c
|
21
|
+
end
|
19
22
|
end,
|
20
23
|
init: OptionParser.new do |opts|
|
21
24
|
opts.banner = "prmd init [options] <resource name>"
|
@@ -24,11 +27,11 @@ commands = {
|
|
24
27
|
end
|
25
28
|
end,
|
26
29
|
render: OptionParser.new do |opts|
|
27
|
-
opts.banner = "prmd
|
30
|
+
opts.banner = "prmd render [options] <combined schema>"
|
28
31
|
opts.on("-p", "--prepend header,overview", Array, "Prepend files to output") do |p|
|
29
32
|
options[:prepend] = p
|
30
33
|
end
|
31
|
-
opts.on("-t", "--template
|
34
|
+
opts.on("-t", "--template templates", String, "Use alternate template") do |t|
|
32
35
|
options[:template] = t
|
33
36
|
end
|
34
37
|
end,
|
@@ -82,7 +85,7 @@ case command
|
|
82
85
|
end
|
83
86
|
schema = Prmd::Schema.new(JSON.parse(data))
|
84
87
|
|
85
|
-
options[:template] = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'prmd', 'templates'
|
88
|
+
options[:template] = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'prmd', 'templates'))
|
86
89
|
|
87
90
|
puts Prmd.render(schema, options)
|
88
91
|
when :init
|
data/docs/schemata.md
CHANGED
@@ -81,7 +81,8 @@ The links array MUST include an object defining each action available. Each acti
|
|
81
81
|
Links that expect a json-encoded body as input MUST also include the following attributes:
|
82
82
|
* `schema` - an object with a `properties` object that MUST include JSON pointers to the definitions for each associated attribute
|
83
83
|
|
84
|
-
|
84
|
+
The `schema` object MAY also include a `required` array to define all attributes for this link, which can not be omitted.
|
85
|
+
If this field is not present, all attributes in this link are considered as optional.
|
85
86
|
|
86
87
|
```javascript
|
87
88
|
{
|
@@ -95,7 +96,8 @@ Schema properties MAY also include a `required` boolean to indicate if an attrib
|
|
95
96
|
"properties": {
|
96
97
|
"owner": { "$ref": "/schema/user#/definitions/identity" },
|
97
98
|
"url": { "$ref": "/schema/resource/definitions/url" }
|
98
|
-
}
|
99
|
+
},
|
100
|
+
"required": [ "owner", "url" ]
|
99
101
|
},
|
100
102
|
"title": "Create"
|
101
103
|
},
|
data/lib/prmd.rb
CHANGED
@@ -11,6 +11,7 @@ require File.join(dir, 'prmd', 'commands', 'render')
|
|
11
11
|
require File.join(dir, 'prmd', 'commands', 'verify')
|
12
12
|
require File.join(dir, 'prmd', 'schema')
|
13
13
|
require File.join(dir, 'prmd', 'version')
|
14
|
+
require File.join(dir, 'prmd', 'template')
|
14
15
|
|
15
16
|
module Prmd
|
16
17
|
end
|
@@ -8,7 +8,17 @@ module Prmd
|
|
8
8
|
[path]
|
9
9
|
end
|
10
10
|
# sort for stable loading on any platform
|
11
|
-
schemata =
|
11
|
+
schemata = []
|
12
|
+
files.sort.each do |file|
|
13
|
+
begin
|
14
|
+
schemata << [file, YAML.load(File.read(file))]
|
15
|
+
rescue
|
16
|
+
$stderr.puts "unable to parse #{file}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
unless schemata.length == files.length
|
20
|
+
exit(1) # one or more files failed to parse
|
21
|
+
end
|
12
22
|
|
13
23
|
data = {
|
14
24
|
'$schema' => 'http://json-schema.org/draft-04/hyper-schema',
|
@@ -45,7 +55,7 @@ module Prmd
|
|
45
55
|
if datum.has_key?('$ref')
|
46
56
|
datum['$ref'] = '#/definitions' + datum['$ref'].gsub('#', '').gsub('/schemata', '')
|
47
57
|
end
|
48
|
-
if datum.has_key?('href')
|
58
|
+
if datum.has_key?('href') && datum['href'].is_a?(String)
|
49
59
|
datum['href'] = datum['href'].gsub('%23', '').gsub(%r{%2Fschemata(%2F[^%]*%2F)}, '%23%2Fdefinitions\1')
|
50
60
|
end
|
51
61
|
datum.each { |k,v| datum[k] = reference_localizer.call(v) }
|
data/lib/prmd/commands/render.rb
CHANGED
@@ -2,11 +2,19 @@ module Prmd
|
|
2
2
|
def self.render(schema, options={})
|
3
3
|
doc = ''
|
4
4
|
|
5
|
+
options[:content_type] ||= 'application/json'
|
6
|
+
|
5
7
|
if options[:prepend]
|
6
8
|
doc << options[:prepend].map {|path| File.read(path)}.join("\n") << "\n"
|
7
9
|
end
|
8
10
|
|
9
|
-
|
11
|
+
template_dir = File::expand_path(options[:template])
|
12
|
+
if not File.directory?(template_dir) # to keep backward compatibility
|
13
|
+
template_dir = File.dirname(options[:template])
|
14
|
+
end
|
15
|
+
options[:template] = template_dir
|
16
|
+
|
17
|
+
doc << Prmd::Template::render('schema.erb', template_dir, {
|
10
18
|
options: options,
|
11
19
|
schema: schema
|
12
20
|
})
|
data/lib/prmd/commands/verify.rb
CHANGED
@@ -1,87 +1,51 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
errors = []
|
4
|
-
errors << verify_schema(schema['id'], schema)
|
5
|
-
schema = Prmd::Schema.new(schema)
|
6
|
-
if schema['properties']
|
7
|
-
schema['properties'].each do |key, value|
|
8
|
-
id, schemata = schema.dereference(value)
|
1
|
+
require "json"
|
2
|
+
require "json_schema"
|
9
3
|
|
10
|
-
|
11
|
-
|
12
|
-
|
4
|
+
module Prmd
|
5
|
+
# These schemas are listed manually and in order because they reference each
|
6
|
+
# other.
|
7
|
+
SCHEMAS = [
|
8
|
+
"schema.json",
|
9
|
+
"hyper-schema.json",
|
10
|
+
"interagent-hyper-schema.json"
|
11
|
+
]
|
12
|
+
|
13
|
+
def self.verify(schema_data)
|
14
|
+
store = init_document_store
|
15
|
+
|
16
|
+
if !(schema_uri = schema_data["$schema"])
|
17
|
+
return ["Missing $schema."]
|
13
18
|
end
|
14
|
-
errors.flatten!
|
15
|
-
end
|
16
19
|
|
17
|
-
|
18
|
-
|
20
|
+
# for good measure, make sure that the schema parses and that its
|
21
|
+
# references can be expanded
|
22
|
+
schema, errors = JsonSchema.parse!(schema_data)
|
23
|
+
return JsonSchema::SchemaError.aggregate(errors) if !schema
|
19
24
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
missing_requirements.each do |missing_requirement|
|
27
|
-
errors << "Missing `#{id}#/#{missing_requirement}`"
|
25
|
+
valid, errors = schema.expand_references(store: store)
|
26
|
+
return JsonSchema::SchemaError.aggregate(errors) if !valid
|
27
|
+
|
28
|
+
if !(meta_schema = store.lookup_schema(schema_uri))
|
29
|
+
return ["Unknown $schema: #{schema_uri}."]
|
28
30
|
end
|
29
31
|
|
30
|
-
errors
|
31
|
-
|
32
|
+
valid, errors = meta_schema.validate(schema_data)
|
33
|
+
return JsonSchema::SchemaError.aggregate(errors) if !valid
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
+
[]
|
36
|
+
end
|
35
37
|
|
36
|
-
|
37
|
-
unless schema['definitions'].has_key?('identity')
|
38
|
-
errors << "Missing `#{id}#/definitions/identity`"
|
39
|
-
end
|
40
|
-
schema['definitions'].each do |key, value|
|
41
|
-
missing_requirements = []
|
42
|
-
unless key == 'identity'
|
43
|
-
%w{description type}.each do |requirement|
|
44
|
-
unless schema['definitions'][key].has_key?(requirement)
|
45
|
-
missing_requirements << requirement
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
# check for example, unless they are nested in array/object
|
50
|
-
type = schema['definitions'][key]['type']
|
51
|
-
unless type.nil? || type.include?('array') || type.include?('object')
|
52
|
-
unless schema['definitions'][key].has_key?('example')
|
53
|
-
missing_requirements << 'example'
|
54
|
-
end
|
55
|
-
end
|
56
|
-
missing_requirements.each do |missing_requirement|
|
57
|
-
errors << "Missing `#{id}#/definitions/#{key}/#{missing_requirement}`"
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
38
|
+
private
|
61
39
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
if link.has_key?('schema')
|
71
|
-
%w{properties type}.each do |requirement|
|
72
|
-
unless link['schema'].has_key?(requirement)
|
73
|
-
missing_requirements << "schema/#{requirement}"
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
missing_requirements.each do |missing_requirement|
|
78
|
-
errors << "Missing #{missing_requirement} in `#{link}` link for `#{id}`"
|
79
|
-
end
|
80
|
-
end
|
81
|
-
else
|
82
|
-
errors << "Missing `#{id}/links`"
|
40
|
+
def self.init_document_store
|
41
|
+
store = JsonSchema::DocumentStore.new
|
42
|
+
SCHEMAS.each do |file|
|
43
|
+
file = File.expand_path("../../../../schemas/#{file}", __FILE__)
|
44
|
+
data = JSON.parse(File.read(file))
|
45
|
+
schema = JsonSchema::Parser.new.parse!(data)
|
46
|
+
schema.expand_references!(store: store)
|
47
|
+
store.add_schema(schema)
|
83
48
|
end
|
84
|
-
|
85
|
-
errors
|
49
|
+
store
|
86
50
|
end
|
87
51
|
end
|
data/lib/prmd/schema.rb
CHANGED
@@ -72,15 +72,19 @@ module Prmd
|
|
72
72
|
end
|
73
73
|
|
74
74
|
def schema_example(schema)
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
_, _schema = dereference(schema)
|
76
|
+
|
77
|
+
if _schema.has_key?('example')
|
78
|
+
_schema['example']
|
79
|
+
elsif _schema.has_key?('properties')
|
78
80
|
example = {}
|
79
|
-
|
81
|
+
_schema['properties'].each do |key, value|
|
80
82
|
_, value = dereference(value)
|
81
83
|
example[key] = schema_value_example(value)
|
82
84
|
end
|
83
85
|
example
|
86
|
+
elsif _schema.has_key?('items')
|
87
|
+
schema_value_example(_schema)
|
84
88
|
end
|
85
89
|
end
|
86
90
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Prmd
|
2
|
+
class Template
|
3
|
+
def self.load(path, base)
|
4
|
+
fallback = File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
5
|
+
|
6
|
+
resolved = File.join(base, path)
|
7
|
+
if not File.exist?(resolved)
|
8
|
+
resolved = File.join(fallback, path)
|
9
|
+
end
|
10
|
+
|
11
|
+
return File.read(resolved)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.render(path, base, *args)
|
15
|
+
template = self.load(path, base)
|
16
|
+
Erubis::Eruby.new(template).result(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -1,13 +1,18 @@
|
|
1
1
|
<%=
|
2
|
-
schemata_template =
|
2
|
+
schemata_template = Prmd::Template::load('schemata.erb', options[:template])
|
3
3
|
|
4
4
|
schema['properties'].map do |resource, property|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
5
|
+
begin
|
6
|
+
_, schemata = schema.dereference(property)
|
7
|
+
Erubis::Eruby.new(schemata_template).result({
|
8
|
+
options: options,
|
9
|
+
resource: resource,
|
10
|
+
schema: schema,
|
11
|
+
schemata: schemata
|
12
|
+
})
|
13
|
+
rescue => e
|
14
|
+
$stdout.puts("Error in resource: #{resource}")
|
15
|
+
raise e
|
16
|
+
end
|
12
17
|
end.join("\n") << "\n"
|
13
18
|
%>
|
@@ -1,74 +1,14 @@
|
|
1
1
|
<%-
|
2
2
|
return unless schemata.has_key?('links') && !schemata['links'].empty?
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
properties.each do |key, value|
|
12
|
-
# found a reference to another element:
|
13
|
-
_, value = schema.dereference(value)
|
14
|
-
if value.has_key?('anyOf')
|
15
|
-
descriptions = []
|
16
|
-
examples = []
|
17
|
-
|
18
|
-
# sort anyOf! always show unique identifier first
|
19
|
-
anyof = value['anyOf'].sort_by do |property|
|
20
|
-
property['$ref'].split('/').last.gsub('id', 'a')
|
21
|
-
end
|
22
|
-
|
23
|
-
anyof.each do |ref|
|
24
|
-
_, nested_field = schema.dereference(ref)
|
25
|
-
descriptions << nested_field['description']
|
26
|
-
examples << nested_field['example']
|
27
|
-
end
|
28
|
-
|
29
|
-
# avoid repetition :}
|
30
|
-
description = if descriptions.size > 1
|
31
|
-
descriptions.first.gsub!(/ of (this )?.*/, "")
|
32
|
-
descriptions[1..-1].map { |d| d.gsub!(/unique /, "") }
|
33
|
-
[descriptions[0...-1].join(", "), descriptions.last].join(" or ")
|
34
|
-
else
|
35
|
-
description = descriptions.last
|
36
|
-
end
|
4
|
+
Prmd::Template::render(File.join('schemata', 'helper.erb'), options[:template], {
|
5
|
+
options: options,
|
6
|
+
resource: resource,
|
7
|
+
schema: schema,
|
8
|
+
schemata: schemata
|
9
|
+
})
|
37
10
|
|
38
|
-
|
39
|
-
attributes << [key, "string", description, example]
|
40
|
-
|
41
|
-
# found a nested object
|
42
|
-
elsif value['type'] == ['object'] && value['properties']
|
43
|
-
nested = extract_attributes(schema, value['properties'])
|
44
|
-
nested.each do |attribute|
|
45
|
-
attribute[0] = "#{key}:#{attribute[0]}"
|
46
|
-
end
|
47
|
-
attributes.concat(nested)
|
48
|
-
# just a regular attribute
|
49
|
-
else
|
50
|
-
description = value['description']
|
51
|
-
if value['enum']
|
52
|
-
description += '<br/><b>one of:</b>' + [*value['enum']].map { |e| "<code>#{e.to_json}</code>" }.join(" or ")
|
53
|
-
end
|
54
|
-
|
55
|
-
if value.has_key?('example')
|
56
|
-
example = [*value['example']].map { |e| "<code>#{e.to_json}</code>" }.join(" or ")
|
57
|
-
elsif value['type'] == ['array'] && value.has_key?('items')
|
58
|
-
example = "<code>#{schema.schema_value_example(value)}</code>"
|
59
|
-
end
|
60
|
-
|
61
|
-
type = if value['type'].include?('null')
|
62
|
-
'nullable '
|
63
|
-
else
|
64
|
-
''
|
65
|
-
end
|
66
|
-
type += (value['format'] || (value['type'] - ['null']).first)
|
67
|
-
attributes << [key, type, description, example]
|
68
|
-
end
|
69
|
-
end
|
70
|
-
return attributes
|
71
|
-
end
|
11
|
+
title = schemata['title'].split(' - ', 2).last
|
72
12
|
%>
|
73
13
|
## <%= title %>
|
74
14
|
<%= schemata['description'] %>
|
@@ -94,83 +34,14 @@
|
|
94
34
|
|
95
35
|
<%- end %>
|
96
36
|
<%- schemata['links'].each do |link, datum| %>
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
-%>
|
109
|
-
### <%= title %> <%= link['title'] %>
|
110
|
-
<%= link['description'] %>
|
111
|
-
|
112
|
-
```
|
113
|
-
<%= link['method'] %> <%= path %>
|
114
|
-
```
|
115
|
-
|
116
|
-
<%- if link.has_key?('schema') && link['schema'].has_key?('properties') %>
|
117
|
-
<%-
|
118
|
-
required, optional = link['schema']['properties'].partition do |k, v|
|
119
|
-
(link['schema']['required'] || []).include?(k)
|
120
|
-
end.map { |partition| Hash[partition] }
|
121
|
-
%>
|
122
|
-
<%- unless required.empty? %>
|
123
|
-
#### Required Parameters
|
124
|
-
<%= Erubis::Eruby.new(link_schema_properties_template).result(params: required, schema: schema) %>
|
125
|
-
|
126
|
-
<%- end %>
|
127
|
-
<%- unless optional.empty? %>
|
128
|
-
#### Optional Parameters
|
129
|
-
<%= Erubis::Eruby.new(link_schema_properties_template).result(params: optional, schema: schema) %>
|
130
|
-
<%- end %>
|
131
|
-
<%- end %>
|
132
|
-
|
133
|
-
#### Curl Example
|
134
|
-
```term
|
135
|
-
<%-
|
136
|
-
data = {}
|
137
|
-
path = path.gsub(/{([^}]*)}/) {|match| '$' + match.gsub(/[{}]/, '').upcase}
|
138
|
-
|
139
|
-
if link.has_key?('schema')
|
140
|
-
data.merge!(schema.schema_example(link['schema']))
|
141
|
-
|
142
|
-
if link['method'].upcase == 'GET' && !data.empty?
|
143
|
-
path << '?'
|
144
|
-
data.sort_by {|k,_| k.to_s }.each do |key, values|
|
145
|
-
[values].flatten.each do |value|
|
146
|
-
path << [key.to_s, CGI.escape(value.to_s)].join('=') << '&'
|
147
|
-
end
|
148
|
-
end
|
149
|
-
path.chop! # remove trailing '&'
|
150
|
-
end
|
151
|
-
end
|
152
|
-
%>
|
153
|
-
$ curl -n -X <%= link['method'] %> <%= schema.href %><%= path %>
|
154
|
-
<%- unless data.empty? || link['method'].upcase == 'GET' %>
|
155
|
-
-H "Content-Type: application/json" \
|
156
|
-
-d '<%= data.to_json %>'
|
157
|
-
<%- end %>
|
158
|
-
```
|
159
|
-
|
160
|
-
#### Response Example
|
161
|
-
```
|
162
|
-
HTTP/1.1 <%= case link['rel']
|
163
|
-
when 'create'
|
164
|
-
'201 Created'
|
165
|
-
else
|
166
|
-
'200 OK'
|
167
|
-
end %>
|
168
|
-
```
|
169
|
-
```javascript```
|
170
|
-
<%- if link['rel'] == 'instances' %>
|
171
|
-
<%= JSON.pretty_generate([schema.schemata_example(resource)]) %>
|
172
|
-
<%- else %>
|
173
|
-
<%= JSON.pretty_generate(schema.schemata_example(resource)) %>
|
174
|
-
<%- end %>
|
175
|
-
```
|
37
|
+
<%=
|
38
|
+
Prmd::Template::render(File.join('schemata', 'link.erb'), options[:template], {
|
39
|
+
options: options,
|
40
|
+
resource: resource,
|
41
|
+
schema: schema,
|
42
|
+
schemata: schemata,
|
43
|
+
link: link,
|
44
|
+
title: title
|
45
|
+
})
|
46
|
+
%>
|
176
47
|
<%- end -%>
|