prmd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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