prmd 0.0.1
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 +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
|