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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MzFkN2NlODhjNzdiNjMyN2E1YTQwMTg0MWRjMTNhMDUwZWE5YTc3OA==
4
+ YTBiMDJmODY4NDAwMTE1ZTMzZTJiY2I2MzA1M2VkMzM2OWZkYmI5NA==
5
5
  data.tar.gz: !binary |-
6
- OGQzODUzMzI3M2U5YjliZDNhNzQ0YjM2ZTUzNzA1MjI1ODQ3Y2NiZA==
6
+ MTc2MjY0MjgzMDEwYzhhYWVmYzZkY2VhNjI0YmM3ZmI4MThhYzFlZA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NzgxZjQyZGMxNDRhMDM3YjI0OGUyYTdiMDVjMzYzNjUzZGJkNDM4MTU0Mjg0
10
- NzMyN2I0ODBiZDNmZTc0NDIzZjU4YWQwMWJkNWQ5NDNkOWYyYjY3MTRkODJm
11
- NzU4OWFlM2NkMWY5NDY5MmYxYjVhN2I4NDk2OWM4NTYxNTliMTc=
9
+ ZDJkMzVkZTk4ZTI1YmU5M2Q2MGVmNzZhZjI0YzFmODYwNDE0YThlYjA1MTYw
10
+ NDI0ZDNhYmFjOTczNTllOTBjZDgyNDYwZGQyMzZhYmVlMzZlMTFjZTIzYWVm
11
+ MmNjOTA0MTIzMWRlNjNmMDU5YTQ3YjZkZjQyZGQ1MzY1YWNlNGI=
12
12
  data.tar.gz: !binary |-
13
- NGVkMDAxMGUxMzk4ODY5NTM4N2VlNjJjMzBjMWEyOWFhY2JhMGIyOTkwMjU0
14
- OWUwNTkyMTlkZWE2MjM4Yjk0NDI5MzRiZDhlOGI3MTU4OWMxZWY3ZWVlNjll
15
- ZGUxYzE0N2QyZmJjZWNjOWM4N2Q3ZGFhMzg1YjdlZjc5ZmRkNzM=
13
+ NmI3N2ViOGIyZDE1NTljODExNDhkY2E2MDlmZThhZTFiZWE2YmFhNmQyNTg0
14
+ MTBjYzczMjg4NDI1OGVjODlkOTI5NzU5YjllYmMyZGJjYzQ2N2I1YWZlMGM2
15
+ MzY3YzIwM2RjYTlkNzk5MzExNjU0NjAxZDUzYTIzODlkZDQ2YjY=
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- prmd (0.3.1)
4
+ prmd (0.4.0)
5
5
  erubis (~> 2.7)
6
+ json_schema (~> 0.1)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
10
11
  erubis (2.7.0)
12
+ json_schema (0.1.4)
11
13
  minitest (5.3.2)
12
14
  rake (10.2.2)
13
15
 
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
4
  Rake::TestTask.new do |t|
5
- t.pattern = "test/*_test.rb"
5
+ t.pattern = "test/**/*_test.rb"
6
6
  end
7
7
 
8
8
  task :default => :test
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 doc [options] <combined schema>"
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 schemata.erb", String, "Use alternate template") do |t|
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', 'schema.erb'))
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
@@ -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
- Schema properties MAY also include a `required` boolean to indicate if an attribute must be present, which is assumed to be false when omitted.
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
  },
@@ -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 = files.sort.map { |file| [file, YAML.load(File.read(file))] }
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) }
@@ -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
- doc << Erubis::Eruby.new(File.read(options[:template])).result({
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
  })
@@ -1,87 +1,51 @@
1
- module Prmd
2
- def self.verify(schema)
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
- errors << verify_schema(id, schemata)
11
- errors << verify_definitions_and_links(id, schemata)
12
- end
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
- def self.verify_schema(id, schema)
18
- errors = []
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
- missing_requirements = []
21
- %w{$schema definitions description links properties title type}.each do |requirement|
22
- unless schema.has_key?(requirement)
23
- missing_requirements << requirement
24
- end
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
- end
32
+ valid, errors = meta_schema.validate(schema_data)
33
+ return JsonSchema::SchemaError.aggregate(errors) if !valid
32
34
 
33
- def self.verify_definitions_and_links(id, schema)
34
- errors = []
35
+ []
36
+ end
35
37
 
36
- if schema['definitions']
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
- if schema['links']
63
- schema['links'].each do |link|
64
- missing_requirements = []
65
- %w{description href method rel title}.each do |requirement|
66
- unless link.has_key?(requirement)
67
- missing_requirements << requirement
68
- end
69
- end
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
@@ -72,15 +72,19 @@ module Prmd
72
72
  end
73
73
 
74
74
  def schema_example(schema)
75
- if schema.has_key?('example')
76
- schema['example']
77
- elsif schema.has_key?('properties')
75
+ _, _schema = dereference(schema)
76
+
77
+ if _schema.has_key?('example')
78
+ _schema['example']
79
+ elsif _schema.has_key?('properties')
78
80
  example = {}
79
- schema['properties'].each do |key, value|
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 = File.read(File.join(File.dirname(options[:template]), 'schemata.erb'))
2
+ schemata_template = Prmd::Template::load('schemata.erb', options[:template])
3
3
 
4
4
  schema['properties'].map do |resource, property|
5
- _, schemata = schema.dereference(property)
6
- Erubis::Eruby.new(schemata_template).result({
7
- options: options,
8
- resource: resource,
9
- schema: schema,
10
- schemata: schemata
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
- link_schema_properties_template = File.read(File.join(File.dirname(options[:template]), 'link_schema_properties.erb'))
5
- title = schemata['title'].split(' - ', 2).last
6
-
7
- def extract_attributes(schema, properties)
8
- attributes = []
9
- properties = properties.sort_by {|k,v| k} # ensure consistent ordering
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
- example = [*examples].map { |e| "<code>#{e.to_json}</code>" }.join(" or ")
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
- path = link['href'].gsub(%r|(\{\([^\)]+\)\})|) do |ref|
99
- ref = ref.gsub('%2F', '/').gsub('%23', '#').gsub(%r|[\{\(\)\}]|, '')
100
- ref_resource = ref.split('#/definitions/').last.split('/').first.gsub('-','_')
101
- identity_key, identity_value = schema.dereference(ref)
102
- if identity_value.has_key?('anyOf')
103
- '{' + ref_resource + '_' + identity_value['anyOf'].map {|r| r['$ref'].split('/').last}.join('_or_') + '}'
104
- else
105
- '{' + ref_resource + '_' + identity_key.split('/').last + '}'
106
- end
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 -%>