scorpio 0.0.4 → 0.1.0

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