1515
1616from __future__ import absolute_import
1717import copy
18+ import re
1819
1920import six
2021
@@ -99,21 +100,118 @@ def _get_values_for_config(self, config_schema_db, config_db):
99100 return config
100101
101102 @staticmethod
102- def _get_object_property_schema (object_schema , additional_properties_keys = None ):
103+ def _get_object_properties_schema (object_schema , object_keys = None ):
103104 """
104- Create a schema for an object property using both additionalProperties and properties.
105+ Create a schema for an object property using all of: properties,
106+ patternProperties, and additionalProperties.
107+
108+ This 'flattens' properties, patternProperties, and additionalProperties
109+ so that we can handle patternProperties and additionalProperties
110+ as if they were defined in properties.
111+ So, every key in object_keys will be assigned a schema
112+ from properties, patternProperties, or additionalProperties.
113+
114+ NOTE: order of precedence: properties, patternProperties, additionalProperties
115+ So, the additionalProperties schema is only used for keys that are not in
116+ properties and that do not match any of the patterns in patternProperties.
117+ And, patternProperties schemas only apply to keys missing from properties.
105118
106119 :rtype: ``dict``
107120 """
108- property_schema = {}
121+ # preserve the order in object_keys
122+ flattened_properties_schema = {key : {} for key in object_keys }
123+
124+ # properties takes precedence over patternProperties and additionalProperties
125+ properties_schema = object_schema .get ("properties" , {})
126+ flattened_properties_schema .update (properties_schema )
127+
128+ # extra_keys has keys that may use patternProperties or additionalProperties
129+ # we remove keys when they have been assigned a schema
130+ extra_keys = set (object_keys ) - set (properties_schema .keys ())
131+
132+ if not extra_keys :
133+ # nothing to check. Don't look at patternProperties or additionalProperties.
134+ return flattened_properties_schema
135+
136+ # match each key against patternPropetties
137+ pattern_properties = object_schema .get ("patternProperties" , {})
138+ # patternProperties should be a dict if defined
139+ if pattern_properties and isinstance (pattern_properties , dict ):
140+ # we need to match all extra_keys against all patterns
141+ # and then compose the per-property schema from all
142+ # the matched patterns' properties.
143+ pattern_properties = {
144+ re .compile (raw_pattern ): pattern_schema
145+ for raw_pattern , pattern_schema in pattern_properties .items ()
146+ }
147+ for key in list (extra_keys ):
148+ key_schemas = []
149+ for pattern , pattern_schema in pattern_properties .items ():
150+ if pattern .search (key ):
151+ key_schemas .append (pattern_schema )
152+ if key_schemas :
153+ # This naive schema composition approximates allOf.
154+ # We can improve this later if someone provides examples that need
155+ # a better allOf schema implementation for patternProperties.
156+ composed_schema = {}
157+ for schema in key_schemas :
158+ composed_schema .update (schema )
159+ # update matched key
160+ flattened_properties_schema [key ] = composed_schema
161+ # don't overwrite matched key's schema with additionalProperties
162+ extra_keys .remove (key )
163+
164+ if not extra_keys :
165+ # nothing else to check. Don't look at additionalProperties.
166+ return flattened_properties_schema
167+
168+ # fill in any remaining keys with additionalProperties
109169 additional_properties = object_schema .get ("additionalProperties" , {})
110170 # additionalProperties can be a boolean or a dict
111171 if additional_properties and isinstance (additional_properties , dict ):
112172 # ensure that these keys are present in the object
113- for key in additional_properties_keys :
114- property_schema [key ] = additional_properties
115- property_schema .update (object_schema .get ("properties" , {}))
116- return property_schema
173+ for key in extra_keys :
174+ flattened_properties_schema [key ] = additional_properties
175+
176+ return flattened_properties_schema
177+
178+ @staticmethod
179+ def _get_array_items_schema (array_schema , items_count = 0 ):
180+ """
181+ Create a schema for array items using both additionalItems and items.
182+
183+ This 'flattens' items and additionalItems so that we can handle additionalItems
184+ as if each additional item was defined in items.
185+
186+ The additionalItems schema will only be used if the items schema is shorter
187+ than items_count. So, when additionalItems is defined, the items schema will be
188+ extended to be at least as long as items_count.
189+
190+ :rtype: ``list``
191+ """
192+ flattened_items_schema = []
193+ items_schema = array_schema .get ("items" , [])
194+ if isinstance (items_schema , dict ):
195+ # with only one schema for all items, additionalItems will be ignored.
196+ flattened_items_schema .extend ([items_schema ] * items_count )
197+ else :
198+ # items is a positional array of schemas
199+ flattened_items_schema .extend (items_schema )
200+
201+ flattened_items_schema_count = len (flattened_items_schema )
202+ if flattened_items_schema_count >= items_count :
203+ # no additional items to account for.
204+ return flattened_items_schema
205+
206+ additional_items = array_schema .get ("additionalItems" , {})
207+ # additionalItems can be a boolean or a dict
208+ if additional_items and isinstance (additional_items , dict ):
209+ # ensure that these indexes are present in the array
210+ flattened_items_schema .extend (
211+ [additional_items ] * (items_count - flattened_items_schema_count )
212+ )
213+
214+ return flattened_items_schema
117215
118216 def _assign_dynamic_config_values (self , schema , config , parent_keys = None ):
119217 """
@@ -137,7 +235,13 @@ def _assign_dynamic_config_values(self, schema, config, parent_keys=None):
137235 if config_is_dict :
138236 # different schema for each key/value pair
139237 schema_item = schema .get (config_item_key , {})
140- if config_is_list :
238+ if config_is_list and isinstance (schema , list ):
239+ # positional schema for list items
240+ try :
241+ schema_item = schema [config_item_key ]
242+ except IndexError :
243+ schema_item = {}
244+ elif config_is_list :
141245 # same schema is shared between every item in the list
142246 schema_item = schema
143247
@@ -149,19 +253,23 @@ def _assign_dynamic_config_values(self, schema, config, parent_keys=None):
149253
150254 # Inspect nested object properties
151255 if is_dictionary :
152- property_schema = self ._get_object_property_schema (
256+ properties_schema = self ._get_object_properties_schema (
153257 schema_item ,
154- additional_properties_keys = config_item_value .keys (),
258+ object_keys = config_item_value .keys (),
155259 )
156260 self ._assign_dynamic_config_values (
157- schema = property_schema ,
261+ schema = properties_schema ,
158262 config = config [config_item_key ],
159263 parent_keys = current_keys ,
160264 )
161265 # Inspect nested list items
162266 elif is_list :
267+ items_schema = self ._get_array_items_schema (
268+ schema_item ,
269+ items_count = len (config [config_item_key ]),
270+ )
163271 self ._assign_dynamic_config_values (
164- schema = schema_item . get ( "items" , {}) ,
272+ schema = items_schema ,
165273 config = config [config_item_key ],
166274 parent_keys = current_keys ,
167275 )
@@ -193,35 +301,72 @@ def _assign_default_values(self, schema, config):
193301
194302 Note: This method mutates config argument in place.
195303
196- :rtype: ``dict``
304+ :rtype: ``dict|list ``
197305 """
198- for schema_item_key , schema_item in six .iteritems (schema ):
306+ schema_is_dict = isinstance (schema , dict )
307+ iterator = schema .items () if schema_is_dict else enumerate (schema )
308+
309+ # _get_*_schema ensures that schema_item is always a dict
310+ for schema_item_key , schema_item in iterator :
199311 has_default_value = "default" in schema_item
200- has_config_value = schema_item_key in config
312+ if isinstance (config , dict ):
313+ has_config_value = schema_item_key in config
314+ else :
315+ has_config_value = schema_item_key < len (config )
201316
202317 default_value = schema_item .get ("default" , None )
203- is_object = schema_item .get ("type" , None ) == "object"
204- has_properties = schema_item .get ("properties" , None )
205- has_additional_properties = schema_item .get ("additionalProperties" , None )
206-
207318 if has_default_value and not has_config_value :
208319 # Config value is not provided, but default value is, use a default value
209320 config [schema_item_key ] = default_value
210321
211- # Inspect nested object properties
212- if is_object and ( has_properties or has_additional_properties ):
213- if not config . get ( schema_item_key , None ):
214- config [ schema_item_key ] = {}
322+ try :
323+ config_value = config [ schema_item_key ]
324+ except ( KeyError , IndexError ):
325+ config_value = None
215326
216- property_schema = self ._get_object_property_schema (
217- schema_item ,
218- additional_properties_keys = config [schema_item_key ].keys (),
219- )
327+ schema_item_type = schema_item .get ("type" , None )
220328
221- self ._assign_default_values (
222- schema = property_schema , config = config [schema_item_key ]
329+ if schema_item_type == "object" :
330+ has_properties = schema_item .get ("properties" , None )
331+ has_pattern_properties = schema_item .get ("patternProperties" , None )
332+ has_additional_properties = schema_item .get (
333+ "additionalProperties" , None
223334 )
224335
336+ # Inspect nested object properties
337+ if (
338+ has_properties
339+ or has_pattern_properties
340+ or has_additional_properties
341+ ):
342+ if not config_value :
343+ config_value = config [schema_item_key ] = {}
344+
345+ properties_schema = self ._get_object_properties_schema (
346+ schema_item ,
347+ object_keys = config_value .keys (),
348+ )
349+
350+ self ._assign_default_values (
351+ schema = properties_schema , config = config_value
352+ )
353+ elif schema_item_type == "array" :
354+ has_items = schema_item .get ("items" , None )
355+ has_additional_items = schema_item .get ("additionalItems" , None )
356+
357+ # Inspect nested array items
358+ if has_items or has_additional_items :
359+ if not config_value :
360+ config_value = config [schema_item_key ] = []
361+
362+ items_schema = self ._get_array_items_schema (
363+ schema_item ,
364+ items_count = len (config_value ),
365+ )
366+ self ._assign_default_values (
367+ schema = items_schema , config = config_value
368+ )
369+
225370 return config
226371
227372 def _get_datastore_value_for_expression (self , key , value , config_schema_item = None ):
0 commit comments