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.
@@ -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,5 @@
1
+ module Prmd
2
+ def self.combine(directory, options={})
3
+ Prmd::Schema.load(directory, options).to_s
4
+ end
5
+ 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
@@ -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