scorpio 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ require 'scorpio/schema_object_base'
2
+
3
+ module Scorpio
4
+ module OpenAPI
5
+ openapi_schema_doc = ::JSON.parse(Scorpio.root.join('documents/swagger.io/v2/schema.json').read)
6
+ openapi_class = proc do |*key|
7
+ Scorpio.class_for_schema(Scorpio::JSON::Node.new_by_type(openapi_schema_doc, key))
8
+ end
9
+
10
+ Document = openapi_class.call()
11
+
12
+ # naming these is not strictly necessary, but is nice to have.
13
+ # generated: puts Scorpio::OpenAPI::Document.schema_document['definitions'].select { |k,v| ['object', nil].include?(v['type']) }.keys.map { |k| "#{k[0].upcase}#{k[1..-1]} = openapi_class.call('definitions', '#{k}')" }
14
+ Info = openapi_class.call('definitions', 'info')
15
+ Contact = openapi_class.call('definitions', 'contact')
16
+ License = openapi_class.call('definitions', 'license')
17
+ Paths = openapi_class.call('definitions', 'paths')
18
+ Definitions = openapi_class.call('definitions', 'definitions')
19
+ ParameterDefinitions = openapi_class.call('definitions', 'parameterDefinitions')
20
+ ResponseDefinitions = openapi_class.call('definitions', 'responseDefinitions')
21
+ ExternalDocs = openapi_class.call('definitions', 'externalDocs')
22
+ Examples = openapi_class.call('definitions', 'examples')
23
+ Operation = openapi_class.call('definitions', 'operation')
24
+ PathItem = openapi_class.call('definitions', 'pathItem')
25
+ Responses = openapi_class.call('definitions', 'responses')
26
+ ResponseValue = openapi_class.call('definitions', 'responseValue')
27
+ Response = openapi_class.call('definitions', 'response')
28
+ Headers = openapi_class.call('definitions', 'headers')
29
+ Header = openapi_class.call('definitions', 'header')
30
+ VendorExtension = openapi_class.call('definitions', 'vendorExtension')
31
+ BodyParameter = openapi_class.call('definitions', 'bodyParameter')
32
+ HeaderParameterSubSchema = openapi_class.call('definitions', 'headerParameterSubSchema')
33
+ QueryParameterSubSchema = openapi_class.call('definitions', 'queryParameterSubSchema')
34
+ FormDataParameterSubSchema = openapi_class.call('definitions', 'formDataParameterSubSchema')
35
+ PathParameterSubSchema = openapi_class.call('definitions', 'pathParameterSubSchema')
36
+ NonBodyParameter = openapi_class.call('definitions', 'nonBodyParameter')
37
+ Parameter = openapi_class.call('definitions', 'parameter')
38
+ Schema = openapi_class.call('definitions', 'schema')
39
+ FileSchema = openapi_class.call('definitions', 'fileSchema')
40
+ PrimitivesItems = openapi_class.call('definitions', 'primitivesItems')
41
+ SecurityRequirement = openapi_class.call('definitions', 'securityRequirement')
42
+ Xml = openapi_class.call('definitions', 'xml')
43
+ Tag = openapi_class.call('definitions', 'tag')
44
+ SecurityDefinitions = openapi_class.call('definitions', 'securityDefinitions')
45
+ BasicAuthenticationSecurity = openapi_class.call('definitions', 'basicAuthenticationSecurity')
46
+ ApiKeySecurity = openapi_class.call('definitions', 'apiKeySecurity')
47
+ Oauth2ImplicitSecurity = openapi_class.call('definitions', 'oauth2ImplicitSecurity')
48
+ Oauth2PasswordSecurity = openapi_class.call('definitions', 'oauth2PasswordSecurity')
49
+ Oauth2ApplicationSecurity = openapi_class.call('definitions', 'oauth2ApplicationSecurity')
50
+ Oauth2AccessCodeSecurity = openapi_class.call('definitions', 'oauth2AccessCodeSecurity')
51
+ Oauth2Scopes = openapi_class.call('definitions', 'oauth2Scopes')
52
+ Title = openapi_class.call('definitions', 'title')
53
+ Description = openapi_class.call('definitions', 'description')
54
+ Default = openapi_class.call('definitions', 'default')
55
+ MultipleOf = openapi_class.call('definitions', 'multipleOf')
56
+ Maximum = openapi_class.call('definitions', 'maximum')
57
+ ExclusiveMaximum = openapi_class.call('definitions', 'exclusiveMaximum')
58
+ Minimum = openapi_class.call('definitions', 'minimum')
59
+ ExclusiveMinimum = openapi_class.call('definitions', 'exclusiveMinimum')
60
+ MaxLength = openapi_class.call('definitions', 'maxLength')
61
+ MinLength = openapi_class.call('definitions', 'minLength')
62
+ Pattern = openapi_class.call('definitions', 'pattern')
63
+ MaxItems = openapi_class.call('definitions', 'maxItems')
64
+ MinItems = openapi_class.call('definitions', 'minItems')
65
+ UniqueItems = openapi_class.call('definitions', 'uniqueItems')
66
+ Enum = openapi_class.call('definitions', 'enum')
67
+ JsonReference = openapi_class.call('definitions', 'jsonReference')
68
+
69
+ class Operation
70
+ attr_accessor :path
71
+ attr_accessor :http_method
72
+
73
+ # there should only be one body parameter; this returns it
74
+ def body_parameter
75
+ (parameters || []).detect do |parameter|
76
+ parameter['in'] == 'body'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,133 @@
1
+ module Scorpio
2
+ class Schema
3
+ def initialize(schema_node)
4
+ @schema_node = schema_node
5
+ end
6
+ attr_reader :schema_node
7
+
8
+ def subschema_for_property(property_name)
9
+ if schema_node['properties'].respond_to?(:to_hash) && schema_node['properties'][property_name].respond_to?(:to_hash)
10
+ self.class.new(schema_node['properties'][property_name].deref)
11
+ else
12
+ if schema_node['patternProperties'].respond_to?(:to_hash)
13
+ _, pattern_schema_node = schema_node['patternProperties'].detect do |pattern, _|
14
+ property_name =~ Regexp.new(pattern) # TODO map pattern to ruby syntax
15
+ end
16
+ end
17
+ if pattern_schema_node
18
+ self.class.new(pattern_schema_node.deref)
19
+ else
20
+ if schema_node['additionalProperties'].is_a?(Scorpio::JSON::Node)
21
+ self.class.new(schema_node['additionalProperties'].deref)
22
+ else
23
+ nil
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ def match_to_object(object)
30
+ # matching oneOf is good here. one schema for one object.
31
+ # matching anyOf is okay. there could be more than one schema matched. it's often just one. if more
32
+ # than one is a match, the problems of allOf occur.
33
+ # matching allOf is questionable. all of the schemas must be matched but we just return the first match.
34
+ # there isn't really a better answer with the current implementation. merging the schemas together
35
+ # is a thought but is not practical.
36
+ %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |someof_key|
37
+ schema_node[someof_key].map(&:deref).map do |someof_node|
38
+ someof_schema = self.class.new(someof_node)
39
+ if someof_schema.validate(object)
40
+ return someof_schema.match_to_object(object)
41
+ end
42
+ end
43
+ end
44
+ return self
45
+ end
46
+
47
+ def subschema_for_index(index)
48
+ if schema_node['items'].is_a?(Scorpio::JSON::ArrayNode)
49
+ if index < schema_node['items'].size
50
+ self.class.new(schema_node['items'][index].deref)
51
+ elsif schema_node['additionalItems'].is_a?(Node)
52
+ self.class.new(schema_node['additionalItems'].deref)
53
+ end
54
+ elsif schema_node['items'].is_a?(Scorpio::JSON::Node)
55
+ self.class.new(schema_node['items'].deref)
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
61
+ def describes_array?
62
+ schema_node['type'] == 'array' ||
63
+ schema_node['items'] ||
64
+ schema_node['additionalItems'] ||
65
+ schema_node['default'].respond_to?(:to_ary) || # TODO make sure this is right
66
+ (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_ary) }) ||
67
+ schema_node['maxItems'] ||
68
+ schema_node['minItems'] ||
69
+ schema_node.key?('uniqueItems') ||
70
+ schema_node['oneOf'].respond_to?(:to_ary) &&
71
+ schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
72
+ schema_node['allOf'].respond_to?(:to_ary) &&
73
+ schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_array? } ||
74
+ schema_node['anyOf'].respond_to?(:to_ary) &&
75
+ schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_array? }
76
+ end
77
+ def describes_hash?
78
+ schema_node['type'] == 'object' ||
79
+ schema_node['required'].respond_to?(:to_ary) ||
80
+ schema_node['properties'].respond_to?(:to_hash) ||
81
+ schema_node['additionalProperties'] ||
82
+ schema_node['patternProperties'] ||
83
+ schema_node['default'].respond_to?(:to_hash) ||
84
+ (schema_node['enum'].respond_to?(:to_ary) && schema_node['enum'].all? { |enum| enum.respond_to?(:to_hash) }) ||
85
+ schema_node['oneOf'].respond_to?(:to_ary) &&
86
+ schema_node['oneOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
87
+ schema_node['allOf'].respond_to?(:to_ary) &&
88
+ schema_node['allOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? } ||
89
+ schema_node['anyOf'].respond_to?(:to_ary) &&
90
+ schema_node['anyOf'].all? { |someof_node| self.class.new(someof_node).describes_hash? }
91
+ end
92
+
93
+ def described_hash_property_names
94
+ Set.new.tap do |property_names|
95
+ if schema_node['properties'].respond_to?(:to_hash)
96
+ property_names.merge(schema_node['properties'].keys)
97
+ end
98
+ if schema_node['required'].respond_to?(:to_ary)
99
+ property_names.merge(schema_node['required'].to_ary)
100
+ end
101
+ # we _could_ look at the properties of 'default' and each 'enum' but ... nah.
102
+ # we should look at dependencies (TODO).
103
+ %w(oneOf allOf anyOf).select { |k| schema_node[k].respond_to?(:to_ary) }.each do |schemas_key|
104
+ schema_node[schemas_key].map(&:deref).map do |someof_node|
105
+ property_names.merge(self.class.new(someof_node).described_hash_property_names)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ def fully_validate(object)
112
+ ::JSON::Validator.fully_validate(schema_node.document, object_to_content(object), fragment: schema_node.fragment)
113
+ end
114
+ def validate(object)
115
+ ::JSON::Validator.validate(schema_node.document, object_to_content(object), fragment: schema_node.fragment)
116
+ end
117
+ def validate!(object)
118
+ ::JSON::Validator.validate!(schema_node.document, object_to_content(object), fragment: schema_node.fragment)
119
+ end
120
+
121
+ def fingerprint
122
+ {class: self.class, schema_node: schema_node}
123
+ end
124
+ include FingerprintHash
125
+
126
+ private
127
+ def object_to_content(object)
128
+ object = object.object if object.is_a?(Scorpio::SchemaObjectBase)
129
+ object = object.content if object.is_a?(Scorpio::JSON::Node)
130
+ object
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,227 @@
1
+ require 'json'
2
+ require 'scorpio/typelike_modules'
3
+
4
+ module Scorpio
5
+ # base class for representing an instance of an object described by a schema
6
+ class SchemaObjectBase
7
+ def initialize(object)
8
+ if object.is_a?(Scorpio::JSON::Node)
9
+ @object = object
10
+ else
11
+ @object = Scorpio::JSON::Node.new_by_type(object, [])
12
+ end
13
+ end
14
+
15
+ attr_reader :object
16
+
17
+ def fragment
18
+ object.fragment
19
+ end
20
+
21
+ def fully_validate
22
+ module_schema.fully_validate(object)
23
+ end
24
+ def validate
25
+ module_schema.validate(object)
26
+ end
27
+ def validate!
28
+ module_schema.validate!(object)
29
+ end
30
+ def inspect
31
+ "\#<#{self.class.name} #{object.inspect}>"
32
+ end
33
+ def pretty_print(q)
34
+ q.instance_exec(self) do |obj|
35
+ text "\#<#{obj.class.name}"
36
+ group_sub {
37
+ nest(2) {
38
+ breakable ' '
39
+ pp obj.object
40
+ }
41
+ }
42
+ breakable ''
43
+ text '>'
44
+ end
45
+ end
46
+
47
+ def fingerprint
48
+ {class: self.class, object: object}
49
+ end
50
+ include FingerprintHash
51
+ end
52
+
53
+ CLASS_FOR_SCHEMA = Hash.new do |h, schema_node_|
54
+ h[schema_node_] = Class.new(SchemaObjectBase).instance_exec(schema_node_) do |schema_node|
55
+ prepend(Scorpio.module_for_schema(schema_node))
56
+ end
57
+ end
58
+
59
+ def self.class_for_schema(schema_node)
60
+ schema_node = schema_node.object if schema_node.is_a?(Scorpio::SchemaObjectBase)
61
+ CLASS_FOR_SCHEMA[schema_node.deref]
62
+ end
63
+
64
+ # this invokes methods of type-like modules (Arraylike, Hashlike) but only if the #object
65
+ # is of the expected class. since the object may be anything - it will just not be a valid
66
+ # instance of its schema - we can't assume that the methods on the Xlike modules will work
67
+ # (e.g. trying to call #each_index on an #object that's not array-like)
68
+ module SchemaObjectMightBeLike
69
+ def inspect(*a, &b)
70
+ if object.is_a?(expected_object_class)
71
+ super
72
+ else
73
+ SchemaObjectBase.instance_method(:inspect).bind(self).call(*a, &b)
74
+ end
75
+ end
76
+ def pretty_print(*a, &b)
77
+ if object.is_a?(expected_object_class)
78
+ super
79
+ else
80
+ SchemaObjectBase.instance_method(:pretty_print).bind(self).call(*a, &b)
81
+ end
82
+ end
83
+ end
84
+ module SchemaObjectBaseHash
85
+ def expected_object_class
86
+ Scorpio::JSON::HashNode
87
+ end
88
+
89
+ # Hash methods
90
+ def each
91
+ return to_enum(__method__) { object.size } unless block_given?
92
+ object.each_key { |k| yield(k, self[k]) }
93
+ self
94
+ end
95
+ include Enumerable
96
+
97
+ def to_hash
98
+ inject({}) { |h, (k, v)| h[k] = v; h }
99
+ end
100
+
101
+ include Hashlike
102
+ include SchemaObjectMightBeLike
103
+
104
+ # hash methods - define only those which do not modify the hash.
105
+
106
+ # methods that don't look at the value; can skip the overhead of #[]
107
+ key_methods = %w(each_key empty? include? has_key? key key? keys length member? size)
108
+ key_methods.each do |method_name|
109
+ define_method(method_name) { |*a, &b| object.public_send(method_name, *a, &b) }
110
+ end
111
+
112
+ # methods which use key and value
113
+ hash_methods = %w(compact each_pair each_value fetch fetch_values has_value? invert
114
+ rassoc reject select to_h transform_values value? values values_at)
115
+ hash_methods.each do |method_name|
116
+ define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
117
+ end
118
+
119
+ def [](property_name_)
120
+ @object_mapped ||= Hash.new do |hash, property_name|
121
+ hash[property_name] = begin
122
+ property_schema = module_schema.subschema_for_property(property_name)
123
+ property_schema = property_schema && property_schema.match_to_object(object[property_name])
124
+
125
+ if property_schema && object[property_name].is_a?(JSON::Node)
126
+ Scorpio.class_for_schema(property_schema.schema_node).new(object[property_name])
127
+ else
128
+ object[property_name]
129
+ end
130
+ end
131
+ end
132
+ @object_mapped[property_name_]
133
+ end
134
+
135
+ def merge(other)
136
+ # we want to strip the containers from this before we merge
137
+ # this is kind of annoying. wish I had a better way.
138
+ other_stripped = ycomb do |striprec|
139
+ proc do |stripobject|
140
+ stripobject = stripobject.object if stripobject.is_a?(Scorpio::SchemaObjectBase)
141
+ stripobject = stripobject.content if stripobject.is_a?(Scorpio::JSON::Node)
142
+ if stripobject.is_a?(Hash)
143
+ stripobject.map { |k, v| {striprec.call(k) => striprec.call(v)} }.inject({}, &:update)
144
+ elsif stripobject.is_a?(Array)
145
+ stripobject.map(&striprec)
146
+ elsif stripobject.is_a?(Symbol)
147
+ stripobject.to_s
148
+ elsif [String, TrueClass, FalseClass, NilClass, Numeric].any? { |c| stripobject.is_a?(c) }
149
+ stripobject
150
+ else
151
+ raise(TypeError, "bad (not jsonifiable) object: #{stripobject.pretty_inspect}")
152
+ end
153
+ end
154
+ end.call(other)
155
+
156
+ self.class.new(object.merge(other_stripped))
157
+ end
158
+ end
159
+
160
+ module SchemaObjectBaseArray
161
+ def expected_object_class
162
+ Scorpio::JSON::ArrayNode
163
+ end
164
+
165
+ def each
166
+ return to_enum(__method__) { object.size } unless block_given?
167
+ object.each_index { |i| yield(self[i]) }
168
+ self
169
+ end
170
+ include Enumerable
171
+
172
+ def to_ary
173
+ to_a
174
+ end
175
+
176
+ include Arraylike
177
+ include SchemaObjectMightBeLike
178
+
179
+ def [](i_)
180
+ # it would make more sense for this to be an array here, but but Array doesn't have a nice memoizing
181
+ # constructor, so it's a hash with integer keys
182
+ @object_mapped ||= Hash.new do |hash, i|
183
+ hash[i] = begin
184
+ index_schema = module_schema.subschema_for_index(i)
185
+ index_schema = index_schema && index_schema.match_to_object(object[i])
186
+
187
+ if index_schema && object[i].is_a?(JSON::Node)
188
+ Scorpio.class_for_schema(index_schema.schema_node).new(object[i])
189
+ else
190
+ object[i]
191
+ end
192
+ end
193
+ end
194
+ @object_mapped[i_]
195
+ end
196
+ end
197
+
198
+ def self.module_for_schema(schema_node_)
199
+ Module.new.tap do |m|
200
+ m.instance_exec(schema_node_) do |module_schema_node|
201
+ unless module_schema_node.is_a?(Scorpio::JSON::Node)
202
+ raise(ArgumentError, "expected instance of Scorpio::JSON::Node; got: #{module_schema_node.pretty_inspect.chomp}")
203
+ end
204
+
205
+ module_schema = Scorpio::Schema.new(module_schema_node)
206
+
207
+ define_method(:module_schema) { module_schema }
208
+ define_singleton_method(:module_schema) { module_schema }
209
+ define_singleton_method(:included) do |includer|
210
+ includer.send(:define_singleton_method, :module_schema) { module_schema }
211
+ end
212
+
213
+ if module_schema.describes_hash?
214
+ include SchemaObjectBaseHash
215
+
216
+ module_schema.described_hash_property_names.each do |property_name|
217
+ define_method(property_name) do
218
+ self[property_name]
219
+ end
220
+ end
221
+ elsif module_schema.describes_array?
222
+ include SchemaObjectBaseArray
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,52 @@
1
+ module Scorpio
2
+ module Hashlike
3
+ def inspect
4
+ object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
5
+ "\#{<#{self.class.name}#{object_group_text}> #{self.map { |k, v| "#{k.inspect} => #{v.inspect}" }.join(', ')}}"
6
+ end
7
+
8
+ def pretty_print(q)
9
+ q.instance_exec(self) do |obj|
10
+ object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
11
+ text "\#{<#{obj.class.name}#{object_group_text}>"
12
+ group_sub {
13
+ nest(2) {
14
+ breakable(obj.any? { true } ? ' ' : '')
15
+ seplist(obj, nil, :each_pair) { |k, v|
16
+ group {
17
+ pp k
18
+ text ' => '
19
+ pp v
20
+ }
21
+ }
22
+ }
23
+ }
24
+ breakable ''
25
+ text '}'
26
+ end
27
+ end
28
+ end
29
+ module Arraylike
30
+ def inspect
31
+ object_group_text = respond_to?(:object_group_text) ? ' ' + self.object_group_text : ''
32
+ "\#[<#{self.class.name}#{object_group_text}> #{self.map { |e| e.inspect }.join(', ')}]"
33
+ end
34
+
35
+ def pretty_print(q)
36
+ q.instance_exec(self) do |obj|
37
+ object_group_text = obj.respond_to?(:object_group_text) ? ' ' + obj.object_group_text : ''
38
+ text "\#[<#{obj.class.name}#{object_group_text}>"
39
+ group_sub {
40
+ nest(2) {
41
+ breakable(obj.any? { true } ? ' ' : '')
42
+ seplist(obj, nil, :each) { |e|
43
+ pp e
44
+ }
45
+ }
46
+ }
47
+ breakable ''
48
+ text ']'
49
+ end
50
+ end
51
+ end
52
+ end