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 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 -%>