lacerda 0.12.5 → 0.13.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eb103cbaef02d7242cb5e6fca4394709496aa9b4
4
- data.tar.gz: 133a8e88e4f034ddf4b1026355e240010a3f687c
3
+ metadata.gz: cf9cdcab9b0b20b1febda478e06f431a48250ec8
4
+ data.tar.gz: 12499578a02c7c5e57c6393d93312a3a21284e9d
5
5
  SHA512:
6
- metadata.gz: 3fa4c39961f7ffc8f39011661fbd2096e994c01e9a072e0a45c56bade9341fa90d725bd60adc41a78b986672eb0cab157fd6339b4d62f7469b590c1ed22b5ef3
7
- data.tar.gz: 31b516bbad9d48abf2e61decb50f719a1faf46da940431369ba297f50adcc345be21e0dfdadab5823593f8c157e837a29f7e07412367c8649cc60cc903fd65f0
6
+ metadata.gz: fbcffea2beb291540f0fe39203f1e50fabbe76ba8ec1021e98b39dd335224dfd9accf4a2457fd49403b63371a99caf945ef6d5ba86c03fda3438093ff9c4dec9
7
+ data.tar.gz: f0e514af65dea7e6b16ea849ff0bfc3d1539eeb1a79fbf943142749829c01d32806fbb228adad90803c384e6737995021bb9437da60b63f274a099712aabd16c
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.13.0
2
+ - enhance some error messages (invalid object types)
3
+ - make non required properties nullable https://github.com/moviepilot/lacerda/pull/21
4
+
1
5
  # 0.12.5
2
6
  - fix https://github.com/moviepilot/zeta/issues/18 (infinite loops
3
7
  for some cases where json pointers are used)
data/lacerda.gemspec CHANGED
@@ -21,8 +21,11 @@ Gem::Specification.new do |spec|
21
21
 
22
22
  spec.add_runtime_dependency "activesupport"
23
23
  spec.add_runtime_dependency "rake", ["~> 10.2"]
24
- spec.add_runtime_dependency "json-schema", ["~> 2.5"]
25
- spec.add_runtime_dependency "redsnow", ["~> 0.4"]
24
+
25
+ # json-schema 2.6.0 validates differently than 2.5.1
26
+ spec.add_runtime_dependency "json-schema", ["~> 2.5.1"]
27
+
28
+ spec.add_runtime_dependency "redsnow", ["~> 0.4.3"]
26
29
  spec.add_runtime_dependency "colorize"
27
30
  spec.add_runtime_dependency "blumquist", ["~> 0.3"]
28
31
 
@@ -37,6 +40,7 @@ Gem::Specification.new do |spec|
37
40
  spec.add_development_dependency "coveralls", ["~> 0.8"]
38
41
  spec.add_development_dependency "codeclimate-test-reporter"
39
42
  spec.add_development_dependency 'pry'
40
- spec.add_development_dependency 'pry-rescue'
41
- spec.add_development_dependency 'pry-stack_explorer'
43
+ # spec.add_development_dependency 'pry-byebug'
44
+ # spec.add_development_dependency 'pry-rescue'
45
+ # spec.add_development_dependency 'pry-stack_explorer'
42
46
  end
@@ -25,36 +25,10 @@ module Lacerda
25
25
  properties_contained?
26
26
  end
27
27
 
28
- private
29
-
30
- def properties_contained?
31
- @contained_schema['properties'].each do |property, contained_property|
32
- resolved_contained_property = data_for_pointer(property, @contained_schema)
33
- containing_property = @containing_schema['properties'][property]
34
- if !containing_property
35
- _e(:ERR_MISSING_DEFINITION, [@initial_location, property])
36
- else
37
- resolved_containing_property = data_for_pointer(
38
- containing_property,
39
- @containing_schema
40
- )
41
- schema_contains?(
42
- resolved_containing_property,
43
- resolved_contained_property,
44
- [property]
45
- )
46
- end
47
- end
48
- @errors.empty?
49
- end
50
-
51
- def _e(error, location, extra = nil)
52
- message = [ERRORS[error], extra].compact.join(": ")
53
- @errors.push(error: error, message: message, location: location.compact.join("/"))
54
- false
55
- end
56
-
57
- def schema_contains?(publish, consume, location = [])
28
+ def schema_contains?(options)
29
+ publish = options[:publish]
30
+ consume = options[:consume]
31
+ location = options[:location] || []
58
32
 
59
33
  # We can only compare types and $refs, so let's make
60
34
  # sure they're there
@@ -76,31 +50,33 @@ module Lacerda
76
50
  # Let's go:
77
51
 
78
52
  # 1)
79
- if (consume['type'] and publish['type'])
80
- if consume['type'] != publish['type']
81
- return _e(:ERR_TYPE_MISMATCH, location, "#{consume['type']} != #{publish['type']}")
53
+ if consume['type'] and publish['type']
54
+ consume_types = ([consume['type']].flatten - ["null"]).sort
55
+ publish_types = [publish['type']].flatten.sort
56
+ if consume_types != publish_types
57
+ return _e(:ERR_TYPE_MISMATCH, location, "Consume types #{consume_types.to_json} not compatible with publish types #{publish_types.to_json}")
82
58
  end
83
59
 
84
60
  # 2)
85
- elsif(consume['$ref'] and publish['$ref'])
61
+ elsif consume['$ref'] and publish['$ref']
86
62
  resolved_consume = resolve_pointer(consume['$ref'], @contained_schema)
87
63
  resolved_publish = resolve_pointer(publish['$ref'], @containing_schema)
88
64
 
89
65
  return _e(:ERR_MISSING_POINTER, location, consume['$ref']) unless resolved_consume
90
66
  return _e(:ERR_MISSING_POINTER, location, publish['$ref']) unless resolved_publish
91
- return schema_contains?(resolved_publish, resolved_consume, location)
67
+ return schema_contains?(publish: resolved_publish, consume: resolved_consume, location: location)
92
68
 
93
69
  # 3)
94
- elsif(consume['type'] and publish['$ref'])
70
+ elsif consume['type'] and publish['$ref']
95
71
  if resolved_ref = resolve_pointer(publish['$ref'], @containing_schema)
96
- return schema_contains?(resolved_ref, consume, location)
72
+ return schema_contains?(publish: resolved_ref, consume: consume, location: location)
97
73
  else
98
74
  return _e(:ERR_MISSING_POINTER, location, publish['$ref'])
99
75
  end
100
76
 
101
77
  # 4)
102
- elsif(consume['$ref'] and publish['type'])
103
- return _e(:ERR_NOT_SUPPORTED, location)
78
+ elsif consume['$ref'] and publish['type']
79
+ return _e(:ERR_NOT_SUPPORTED, location, nil)
104
80
  end
105
81
 
106
82
  # Make sure required properties in consume are required in publish
@@ -111,24 +87,126 @@ module Lacerda
111
87
 
112
88
  # We already know that publish and consume's type are equal
113
89
  # but if they're objects, we need to do some recursion
114
- if consume['type'] == 'object'
115
- consume['properties'].each do |property, schema|
116
- return _e(:ERR_MISSING_PROPERTY, location, property) unless publish['properties'][property]
117
- return false unless schema_contains?(publish['properties'][property], schema, location + [property])
90
+ if [consume['type']].flatten.include?('object')
91
+
92
+ # An object can either be described by its properties
93
+ # like this:
94
+ #
95
+ # (1) { "type": "object", "properties": { "active": { "type": "boolean" } }
96
+ #
97
+ # or by allowing a bunch of other types like this:
98
+ #
99
+ # (2) { "type": "object", "oneOf": [ {"$ref": "#/definitions/foo"}, {"type": "null"} ]
100
+ #
101
+ # So we need to take care of both cases for both "sides"
102
+ # (publish and consume), so 4 cases in total.
103
+ #
104
+ # First, the easy case:
105
+ if consume['properties'] and publish['properties']
106
+ consume['properties'].each do |property, schema|
107
+ return _e(:ERR_MISSING_PROPERTY, location, property) unless publish['properties'][property]
108
+ return false unless schema_contains?(publish: publish['properties'][property], consume: schema, location: location + [property])
109
+ end
110
+
111
+ # Now on to the trickier case, both have 'oneOf's:
112
+ #
113
+ # For each possible object type from the publish schema we have
114
+ # to check if we find a compatible type in the consume schema.
115
+ #
116
+ # It's not sufficient to just compare the names of the objects,
117
+ # because they might be different in the publish and consume
118
+ # schemas.
119
+ elsif publish['oneOf'] and consume['oneOf']
120
+ publish_types = publish['oneOf']
121
+ consume_types = [consume['oneOf']].flatten.compact
122
+
123
+ # Check all publish types for a compatible consume type
124
+ publish_types.each do |publish_type|
125
+ original_errors = @errors
126
+ @errors = []
127
+ compatible_consume_type_found = false
128
+ consume_types.each do |consume_type|
129
+ next unless publish_type == consume_type or # Because null types are stripped in schema_contains
130
+ schema_contains?(publish: publish_type, consume: consume_type, location: location + [publish_type])
131
+ compatible_consume_type_found = true
132
+ end
133
+ @errors = original_errors
134
+ return _e(:ERR_MISSING_MULTI_PUBLISH_MULTI_CONSUME, location, publish_type) unless compatible_consume_type_found
135
+ end
136
+
137
+ # Mixed case 1/2:
138
+ elsif consume['oneOf'] and publish['properties']
139
+ consume_types = ([consume['oneOf']].flatten - [{"type" => "null"}]).sort
140
+ compatible_consume_type_found = false
141
+ original_errors = @errors
142
+ @errors = []
143
+ consume_types.each do |consume_type|
144
+ next unless schema_contains?(publish: publish, consume: consume_type, location: location)
145
+ compatible_consume_type_found = true
146
+ end
147
+ @errors = original_errors
148
+ return _e(:ERR_MISSING_SINGLE_PUBLISH_MULTI_CONSUME, location, publish['type']) unless compatible_consume_type_found
149
+
150
+ # Mixed case 2/2:
151
+ elsif consume['properties'] and publish['oneOf']
152
+ publish_types = ([publish['oneOf']].flatten - [{"type" => "null"}]).sort
153
+ incompatible_publish_type= nil
154
+ original_errors = @errors
155
+ @errors = []
156
+ publish_types.each do |publish_type|
157
+ next if schema_contains?(publish: publish_type, consume: consume, location: location)
158
+ incompatible_publish_type = publish_type
159
+ end
160
+ @errors = original_errors
161
+ return _e(:ERR_MISSING_MULTI_PUBLISH_SINGLE_CONSUME, location, incompatible_publish_type) if incompatible_publish_type
162
+
163
+ # We don't know how to handle this 😳
164
+ # an object can either have "properties" or "oneOf", if the schema has anything else, we break
165
+ else
166
+ return _e(:ERR_NOT_SUPPORTED, location, "Consume schema didn't have properties defined and publish schema no oneOf")
118
167
  end
119
168
  end
120
169
 
121
170
  if consume['type'] == 'array'
122
171
  sorted_publish = publish['items'].sort
123
172
  consume['items'].sort.each_with_index do |item, i|
124
- next if schema_contains?(sorted_publish[i], item)
125
- return _e(:ERR_ARRAY_ITEM_MISMATCH, location)
173
+ next if schema_contains?(publish: sorted_publish[i], consume: item)
174
+ return _e(:ERR_ARRAY_ITEM_MISMATCH, location, nil)
126
175
  end
127
176
  end
128
177
 
129
178
  true
130
179
  end
131
180
 
181
+ private
182
+
183
+ def properties_contained?
184
+ @contained_schema['properties'].each do |property, contained_property|
185
+ resolved_contained_property = data_for_pointer(property, @contained_schema)
186
+ containing_property = @containing_schema['properties'][property]
187
+ if !containing_property
188
+ _e(:ERR_MISSING_DEFINITION, [@initial_location, property], "(in publish.mson)")
189
+ else
190
+ resolved_containing_property = data_for_pointer(
191
+ containing_property,
192
+ @containing_schema
193
+ )
194
+ schema_contains?(
195
+ publish: resolved_containing_property,
196
+ consume: resolved_contained_property,
197
+ location: [property]
198
+ )
199
+ end
200
+ end
201
+ @errors.empty?
202
+ end
203
+
204
+ def _e(error, location, extra = nil)
205
+ message = [ERRORS[error], extra].compact.join(": ")
206
+ @errors.push(error: error, message: message, location: location.compact.join("/"))
207
+ false
208
+ end
209
+
132
210
  # Resolve pointer data idempotent(ally?). It will resolve
133
211
  #
134
212
  # "foobar"
@@ -56,7 +56,7 @@ module Lacerda
56
56
  # or else the json validator gem will go into an endless loop
57
57
  object_schema =schema_dup['definitions'].delete underscored_name.to_s
58
58
 
59
- raise Lacerda::Service::InvalidObjectTypeError.new("Invalid object type: #{underscored_name.to_s.to_json}") unless object_schema
59
+ raise Lacerda::Service::InvalidObjectTypeError.new("Unknown object type: #{underscored_name.to_s.to_json} - did you specify it in consume.mson?") unless object_schema
60
60
 
61
61
  # Copy the definitions of our schema into the schema for the
62
62
  # object in case its properties include json pointers
@@ -57,16 +57,23 @@ module Lacerda
57
57
  return unless @data['sections']
58
58
  return unless @data['sections'].length > 0
59
59
  members = @data['sections'].select{|d| d['class'] == 'memberType' }.first['content'].select{|d| d['class'] == 'property' }
60
+
61
+ # Iterate over each property
60
62
  members.each do |s|
63
+
64
+ # Pluck some things out of the AST
61
65
  content = s['content']
62
66
  type_definition = content['valueDefinition']['typeDefinition']
63
67
  type = type_definition['typeSpecification']['name']
68
+ attributes = type_definition['attributes'] || []
69
+ is_required = attributes.include?('required')
64
70
 
71
+ # Prepare the json schema fragment
65
72
  spec = {}
66
73
  name = Lacerda.underscore(content['name']['literal'])
67
74
 
68
75
  # This is either type: primimtive or $ref: reference_name
69
- spec.merge!(primitive_or_reference(type))
76
+ spec.merge!(primitive_or_reference(type, is_required))
70
77
 
71
78
  # We might have a description
72
79
  spec['description'] = content['description']
@@ -74,7 +81,7 @@ module Lacerda
74
81
  # If it's an array, we need to pluck out the item types
75
82
  if type == 'array'
76
83
  nestedTypes = type_definition['typeSpecification']['nestedTypes']
77
- spec['items'] = nestedTypes.map{|t| primitive_or_reference(t) }
84
+ spec['items'] = nestedTypes.map{|t| primitive_or_reference(t, is_required) }
78
85
 
79
86
  # If it's an object, we need recursion
80
87
  elsif type == 'object'
@@ -85,20 +92,28 @@ module Lacerda
85
92
  end
86
93
  end
87
94
 
95
+ # Add the specification of this property to the schema
88
96
  @schema['properties'][name] = spec
89
- if attributes = type_definition['attributes']
90
- @schema['required'] << name if attributes.include?('required')
91
- end
97
+
98
+ # Mark the property as required
99
+ @schema['required'] << name if is_required
92
100
  end
93
101
  end
94
102
 
95
- def primitive_or_reference(type)
96
- return { 'type' => 'object' } if type.blank?
103
+ def primitive_or_reference(type, is_required)
104
+ return { 'type' => 'object' } if type.blank?
97
105
 
98
106
  if PRIMITIVES.include?(type)
99
- { 'type' => type }
107
+ types = [type]
108
+ types << 'null' unless is_required
109
+ { 'type' => types }
100
110
  else
101
- { '$ref' => "#/definitions/#{self.class.scope(@scope, type['literal'])}" }
111
+ types = [{'$ref' => "#/definitions/#{self.class.scope(@scope, type['literal'])}" }]
112
+ types << { 'type' => 'null' } unless is_required
113
+ {
114
+ 'type' => 'object',
115
+ 'oneOf' => types
116
+ }
102
117
  end
103
118
  end
104
119
 
@@ -32,7 +32,11 @@ module Lacerda
32
32
  # It's critical to delete this object from the definitions
33
33
  # or else the json validator gem will go into an endless loop
34
34
  object_schema = schema_dup['definitions'].delete scoped_name.to_s
35
- raise Lacerda::Service::InvalidObjectTypeError.new(scoped_name) unless object_schema
35
+
36
+ unless object_schema
37
+ msg = "Unknown object type: #{scoped_name.to_s.to_json} - did you specify it in publish.mson?"
38
+ raise Lacerda::Service::InvalidObjectTypeError.new(msg)
39
+ end
36
40
 
37
41
  # Copy the definitions of our schema into the schema for the
38
42
  # object in case its properties include json pointers
@@ -1,3 +1,3 @@
1
1
  module Lacerda
2
- VERSION = '0.12.5'
2
+ VERSION = '0.13.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lacerda
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.5
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jannis Hermanns
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-12-09 00:00:00.000000000 Z
11
+ date: 2016-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -44,28 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.5'
47
+ version: 2.5.1
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.5'
54
+ version: 2.5.1
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: redsnow
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '0.4'
61
+ version: 0.4.3
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '0.4'
68
+ version: 0.4.3
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: colorize
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -220,34 +220,6 @@ dependencies:
220
220
  - - ">="
221
221
  - !ruby/object:Gem::Version
222
222
  version: '0'
223
- - !ruby/object:Gem::Dependency
224
- name: pry-rescue
225
- requirement: !ruby/object:Gem::Requirement
226
- requirements:
227
- - - ">="
228
- - !ruby/object:Gem::Version
229
- version: '0'
230
- type: :development
231
- prerelease: false
232
- version_requirements: !ruby/object:Gem::Requirement
233
- requirements:
234
- - - ">="
235
- - !ruby/object:Gem::Version
236
- version: '0'
237
- - !ruby/object:Gem::Dependency
238
- name: pry-stack_explorer
239
- requirement: !ruby/object:Gem::Requirement
240
- requirements:
241
- - - ">="
242
- - !ruby/object:Gem::Version
243
- version: '0'
244
- type: :development
245
- prerelease: false
246
- version_requirements: !ruby/object:Gem::Requirement
247
- requirements:
248
- - - ">="
249
- - !ruby/object:Gem::Version
250
- version: '0'
251
223
  description: Specify which objects your services publish or consume in MSON (markdown)
252
224
  and let this gem validate these contracts.
253
225
  email: