Class: Lacerda::Compare::JsonSchema
- Inherits:
-
Object
- Object
- Lacerda::Compare::JsonSchema
- Defined in:
- lib/lacerda/compare/json_schema.rb
Constant Summary collapse
- ERRORS =
{ :ERR_ARRAY_ITEM_MISMATCH => "The items in the published array don't match the consumer's specification.", :ERR_MISSING_DEFINITION => "The publish specification is missing a type defined in the consumer's specification.", :ERR_MISSING_POINTER => "A JSON pointer could not be resolved.", :ERR_MISSING_PROPERTY => "The published object is missing a property required by your specification.", :ERR_MISSING_REQUIRED => "The published object has an optional property that you marked as required in your specification.", :ERR_MISSING_TYPE_AND_REF_AND_ONE_OF => 'A property has to either have a "type", "oneOf" or "$ref" property.', :ERR_TYPE_MISMATCH => "The published object has a property with a different type than the consumer's specification.", :ERR_NOT_IMPLEMENTED => "Not implemented.", :ERR_NOT_SUPPORTED => 'I don\'t yet know what to do when the consumer\'s specification has a "$ref" defined and the publisher\'s specification has a "type".' }
Instance Attribute Summary collapse
-
#errors ⇒ Object
readonly
Returns the value of attribute errors.
Instance Method Summary collapse
- #contains?(contained_schema, initial_location = nil) ⇒ Boolean
-
#initialize(containing_schema) ⇒ JsonSchema
constructor
A new instance of JsonSchema.
- #schema_contains?(options) ⇒ Boolean
Constructor Details
#initialize(containing_schema) ⇒ JsonSchema
Returns a new instance of JsonSchema.
18 19 20 21 |
# File 'lib/lacerda/compare/json_schema.rb', line 18 def initialize(containing_schema) @containing_schema = containing_schema @errors = [] end |
Instance Attribute Details
#errors ⇒ Object (readonly)
Returns the value of attribute errors.
16 17 18 |
# File 'lib/lacerda/compare/json_schema.rb', line 16 def errors @errors end |
Instance Method Details
#contains?(contained_schema, initial_location = nil) ⇒ Boolean
23 24 25 26 27 28 |
# File 'lib/lacerda/compare/json_schema.rb', line 23 def contains?(contained_schema, initial_location = nil) @errors = [] @initial_location = initial_location @contained_schema = contained_schema properties_contained? end |
#schema_contains?(options) ⇒ Boolean
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/lacerda/compare/json_schema.rb', line 30 def schema_contains?() publish = [:publish] consume = [:consume] location = [:location] || [] return false unless publish and consume # We can only compare types and $refs, so let's make # sure they're there return _e(:ERR_MISSING_TYPE_AND_REF_AND_ONE_OF, location) unless (consume['type'] or consume['$ref'] or consume['oneOf']) and (publish['type'] or publish['$ref'] or publish['oneOf']) # There's four possibilities here: # # 1) publish and consume have a type defined # 2) publish and consume have a $ref defined # 3) publish has a $ref defined, and consume an inline object # 4) consume has a $ref defined, and publish an inline object # (we don't support this yet, as otherwise couldn't check for # missing definitions, because we could never know if something # specified in the definitions of the consuming schema exists in # the publishing schema as an inline property somewhere). # TODO: check if what I just said makes sense. I'm not sure anymore. # Let's go: # 1) if consume['type'] and publish['type'] consume_types = ([consume['type']].flatten).sort publish_types = [publish['type']].flatten.sort if !(publish_types - consume_types).blank? return _e(:ERR_TYPE_MISMATCH, location, "Consume types #{consume_types.to_json} not compatible with publish types #{publish_types.to_json}") end # 2) elsif consume['$ref'] and publish['$ref'] resolved_consume = resolve_pointer(consume['$ref'], @contained_schema) resolved_publish = resolve_pointer(publish['$ref'], @containing_schema) return _e(:ERR_MISSING_POINTER, location, consume['$ref']) unless resolved_consume return _e(:ERR_MISSING_POINTER, location, publish['$ref']) unless resolved_publish return schema_contains?(publish: resolved_publish, consume: resolved_consume, location: location) # 3) elsif consume['type'] and publish['$ref'] if resolved_ref = resolve_pointer(publish['$ref'], @containing_schema) return schema_contains?(publish: resolved_ref, consume: consume, location: location) else return _e(:ERR_MISSING_POINTER, location, publish['$ref']) end # 4) elsif consume['$ref'] and publish['type'] return _e(:ERR_NOT_SUPPORTED, location, nil) end # Make sure required properties in consume are required in publish consume_required = consume['required'] || [] publish_required = publish['required'] || [] missing = (consume_required - publish_required) return _e(:ERR_MISSING_REQUIRED, location, missing.to_json) unless missing.empty? # We already know that publish and consume's type are equal # but if they're objects, we need to do some recursion isnt_a_primitive = [consume['type']].flatten.include?('object') || consume['oneOf'] || publish['oneOf'] if isnt_a_primitive # An object can either be described by its properties # like this: # # (1) { "type": "object", "properties": { "active": { "type": "boolean" } } # # or by allowing a bunch of other types like this: # # (2) { "type": "object", "oneOf": [ {"$ref": "#/definitions/foo"}, {"type": "null"} ] # # So we need to take care of both cases for both "sides" # (publish and consume), so 4 cases in total. # # First, the easy case: if consume['properties'] and publish['properties'] consume['properties'].each do |property, schema| return _e(:ERR_MISSING_PROPERTY, location, property) unless publish['properties'][property] return false unless schema_contains?(publish: publish['properties'][property], consume: schema, location: location + [property]) end # Now on to the trickier case, both have 'oneOf's: # # For each possible object type from the publish schema we have # to check if we find a compatible type in the consume schema. # # It's not sufficient to just compare the names of the objects, # because they might be different in the publish and consume # schemas. elsif publish['oneOf'] and consume['oneOf'] publish_types = publish['oneOf'] consume_types = [consume['oneOf']].flatten.compact # Check all publish types for a compatible consume type publish_types.each do |publish_type| errors = [] consume_types.any? do |consume_type| errors = compare_sub_types(publish_type, consume_type, location + [publish_type]) errors.empty? end if errors.any? # As there is only one type in each oneOf, we can give more specific error. # TODO: add this to other cases if publish_types.size == 1 && consume_types.size == 1 @errors.push(*errors) else _e(:ERR_MISSING_MULTI_PUBLISH_MULTI_CONSUME, location, publish_type) end return false end end # Mixed case 1/2: elsif consume['oneOf'] and publish['properties'] consume_types = ([consume['oneOf']].flatten - [{"type" => "null"}]).sort compatible_consume_type_found = false original_errors = @errors @errors = [] consume_types.each do |consume_type| next unless schema_contains?(publish: publish, consume: consume_type, location: location) compatible_consume_type_found = true end @errors = original_errors unless compatible_consume_type_found return _e(:ERR_MISSING_SINGLE_PUBLISH_MULTI_CONSUME, location, publish['type']) end # Mixed case 2/2: elsif consume['properties'] and publish['oneOf'] publish_types = ([publish['oneOf']].flatten - [{"type" => "null"}]).sort incompatible_publish_type= nil original_errors = @errors @errors = [] publish_types.each do |publish_type| next if schema_contains?(publish: publish_type, consume: consume, location: location) incompatible_publish_type = publish_type end @errors = original_errors if incompatible_publish_type return _e(:ERR_MISSING_MULTI_PUBLISH_SINGLE_CONSUME, location, incompatible_publish_type) end # We don't know how to handle this 😳 # an object can either have "properties" or "oneOf", if the schema has anything else, we break else return _e(:ERR_NOT_SUPPORTED, location, "Consume schema didn't have properties defined and publish schema no oneOf") end end if consume['type'] == 'array' && publish['type'] == 'array' if !consume['items'].is_a?(Hash) || !publish['items'].is_a?(Hash) return _e(:ERR_NOT_IMPLEMENTED, location, "'items' can only be hash (schema)") elsif !schema_contains?(publish: publish['items'], consume: consume['items']) return _e(:ERR_ARRAY_ITEM_MISMATCH, location, nil) end end true end |