prmd 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -%>
|