def generate_integer(self, integer_spec, spec_name_stack=None): """ Generate an integer from the given specification. :param integer_spec: An integer specification :param spec_name_stack: A specification name stack, for reference loop detection. Unused but included for API compatibility with object/array generators. :return: An int :raises stix2generator.exceptions.ObjectGenerationError: If a generation error occurs """ min_, is_min_exclusive, max_, is_max_exclusive = \ _process_numeric_min_max_properties( integer_spec, self.config.number_min, self.config.is_number_min_exclusive, self.config.number_max, self.config.is_number_max_exclusive ) # Guess I won't assume the user expressed the bounds as ints, so I # need to convert to ints and check the resulting bounds. The # call above to process min/max properties doesn't assume we require # ints. if int(min_) == min_: min_ = int(min_) if is_min_exclusive: min_ += 1 else: min_ = int(math.ceil(min_)) if int(max_) == max_: max_ = int(max_) if is_max_exclusive: max_ -= 1 else: max_ = int(math.floor(max_)) if min_ > max_: raise ObjectGenerationError( "no integers exist in the specified interval", "integer") return random.randint(min_, max_)
def __generate_semantic(self, spec, value_constraint): """ Generate from a semantic-type spec. :param spec: The spec :param value_constraint: A ValueConstraint instance representing some additional constraint to be honored by the generator. This is derived from a value co-constraint expression. If None, there is no additional constraint. :return: The generated value :raises stix2generator.exceptions.SemanticValueTypeMismatchError: If the semantic produces a value which doesn't agree with the spec's declared type. :raises stix2generator.exceptions.ObjectGenerationError: If the semantic name isn't found in any of this generator's semantic providers """ semantic = spec[ stix2generator.generation.semantics.SEMANTIC_PROPERTY_NAME ] if semantic in self.__semantics: provider = self.__semantics[semantic] value = provider.create_semantic(spec, self, value_constraint) # Should check that the implementation created the right type of # value. actual_type = _json_type_from_python_type(type(value)) if actual_type != spec["type"]: raise SemanticValueTypeMismatchError( semantic, actual_type, value, spec["type"] ) else: raise ObjectGenerationError( "unrecognized semantic: " + semantic ) return value
def _get_properties_to_include(object_spec, optional_property_probability, minimize_ref_properties): """ Determine which object properties to include, based on required/optional choices and any defined presence co-constraints. :param object_spec: The object spec :param optional_property_probability: The probability an optional property should be included. Must be a number from 0 to 1. :param minimize_ref_properties: True if we should minimize optional reference properties. False if they should receive no special treatment. :return: The property names, as a set of strings :raises stix2generator.exceptions.PresenceCoconstraintError: If an invalid presence co-constraint is found :raises stix2generator.exceptions.UndefinedPropertyError: If a reference to an undefined property or group is found in the "required" or "optional" property value of the spec :raises stix2generator.exceptions.ObjectGenerationError: If a reference to a grouped property is found """ prop_specs = object_spec.get("properties", {}) required_names = object_spec.get("required") optional_names = object_spec.get("optional") if required_names is not None and optional_names is not None: raise ObjectGenerationError( '"required" and "optional" can\'t both be present') # If neither optional nor required names are specified, all # properties/groups will be required. elif required_names is None and optional_names is None: # empty optional set = all required optional_names = set() # Convert to sets to remove dupes elif required_names is not None: required_names = set(required_names) elif optional_names is not None: optional_names = set(optional_names) group_coconstraints, dependency_coconstraints = \ _get_presence_coconstraints(object_spec) # Detect errors in the required/optional prop list: all must be # defined, and grouped properties must not be referenced req_or_opt = required_names if required_names is not None \ else optional_names defined_prop_names = prop_specs.keys() defined_group_names = group_coconstraints.keys() grouped_property_names = set( itertools.chain.from_iterable( coco.property_names for coco in group_coconstraints.values())) undef_name_errors = req_or_opt - defined_prop_names - defined_group_names if undef_name_errors: raise UndefinedPropertyError(undef_name_errors) grouped_prop_errors = req_or_opt & grouped_property_names if grouped_prop_errors: raise ObjectGenerationError( "Property(s) are grouped and cannot be referenced" " individually: {}".format(", ".join( "{}".format(p) for p in grouped_prop_errors))) # Include all ungrouped property names and property group names in # the same "pool" of names one can specify as required or optional. name_pool = (defined_prop_names - grouped_property_names) \ | defined_group_names # Get set of optional names (whether they specified "required" or # "optional" in the spec). effectively_optional_names = optional_names if optional_names is not None \ else name_pool - required_names # Start out the set of names to include with all required ones. names_to_include = required_names if required_names is not None \ else name_pool - effectively_optional_names # And then maybe add some optional ones. for name in effectively_optional_names: is_group = name in defined_group_names is_ref = name.endswith("_ref") or name.endswith("_refs") can_include = False if minimize_ref_properties: if is_group: if group_coconstraints[name].can_satisfy_without_refs(): can_include = True elif not is_ref: can_include = True else: can_include = True if can_include and random.random() < optional_property_probability: names_to_include.add(name) # Incorporate the "dependencies": add any other properties we # require for dep_key, dep_names in dependency_coconstraints.items(): if dep_key in names_to_include: names_to_include.update(dep_names) # For any names which are property groups, expand them to the # component properties according to their co-constraints # ... can't modify a set as you iterate! So need a temp set. temp_set = set() for name in names_to_include: if name in group_coconstraints: temp_set.update(group_coconstraints[name].choose_properties( optional_property_probability, minimize_ref_properties)) else: temp_set.add(name) names_to_include = temp_set return names_to_include
def generate_from_spec(self, spec, expected_type=None, spec_name_stack=None, value_constraint=None): """ Generate a value based on the given specification, which need not exist under any particular name in this generator's registry. :param spec: The specification, as parsed JSON :param expected_type: If the spec should be for a particular JSON type, that type. If it doesn't matter, pass None. :param spec_name_stack: A stack of previously-visited specification names, used for reference loop detection. Pass None to start a new stack. :param value_constraint: A ValueConstraint instance representing some additional constraint to be honored by the generator. This is derived from a value co-constraint expression. If None, there is no additional constraint. :return: The generated value :raises stix2generator.exceptions.UnrecognizedJSONTypeError: If given a non-const dict spec whose declared type is not recognized as a JSON type, or if expected_type is given and not a recognized JSON type. :raises stix2generator.exceptions.TypeMismatchError: If expected_type is given and the spec type doesn't match. :raises stix2generator.exceptions.ObjectGenerationError: For various ways the given spec is invalid. Other types of errors are also wrapped/chained from this exception type (if possible) so that we get decoration with extra info from higher stack frames, which is useful for diagnosing where those problems occur. """ spec_type = _get_spec_type(spec) if expected_type: if expected_type not in _JSON_TYPES: raise UnrecognizedJSONTypeError(expected_type) # There really should be some flexibility for numeric types: if # number is expected, integers should be accepted too... if spec_type != expected_type: raise TypeMismatchError(expected_type, spec_type) # If not a dict, the spec IS the desired value. It's an easy way to # produce fixed values. if not isinstance(spec, dict): value = spec # The other way: use "const", like in json-schema. elif "const" in spec: value = spec["const"] else: semantic_name = spec.get( stix2generator.generation.semantics.SEMANTIC_PROPERTY_NAME) try: if semantic_name: value = self.__generate_semantic(spec, value_constraint) else: value = self.__generate_plain(spec, spec_name_stack, value_constraint) except ObjectGenerationError as e: # In a recursive context, set this at the deepest nesting # level only. Also, I think it's better to use the semantic # name as the type name in error messages, for semantic specs. if not e.spec_type: e.spec_type = semantic_name or spec_type raise except Exception as e: raise ObjectGenerationError( "An error occurred during generation: {}: {}".format( type(e).__name__, str(e)), semantic_name or spec_type) from e return value
def _process_numeric_min_max_properties(spec, default_min, is_default_min_exclusive, default_max, is_default_max_exclusive): """ Factors out a rather large chunk of code for validating and processing the min/max properties on numbers and integers. Maybe we need a JSON-Schema for specifications and validate against that, to reduce the amount of hand-written validation code we need to write... :param spec: A number or integer spec :param default_min: If the spec doesn't specify a minimum, use this as the default. :param is_default_min_exclusive: Whether default_min, if it is used, is an exclusive bound. :param default_max: If the spec doesn't specify a maximum, use this as the default. :param is_default_max_exclusive: Whether default_max, if it is used, is an exclusive bound. :return: A (num, bool, num, bool) 4-tuple giving the bounds and whether each bound is exclusive or not: (min, is_min_exclusive, max, is_max_exclusive) :raises stix2generator.exceptions.ObjectGenerationError: For various types of problems with numeric specifications """ if "minimum" in spec and "exclusiveMinimum" in spec: raise ObjectGenerationError( "minimum and exclusiveMinimum can't both be present") if "maximum" in spec and "exclusiveMaximum" in spec: raise ObjectGenerationError( "maximum and exclusiveMaximum can't both be present") min_given = any(p in spec for p in ("minimum", "exclusiveMinimum")) max_given = any(p in spec for p in ("maximum", "exclusiveMaximum")) # I think this check is necessary since user-specified min/max could well # be out of order w.r.t. defaults, producing unexpected errors. What would # users expect the other bound to be anyway, if they only gave one bound? if (min_given and not max_given) or (max_given and not min_given): raise ObjectGenerationError( "can't give minimum without a maximum, or vice versa") if "minimum" in spec: min_ = spec["minimum"] is_min_exclusive = False elif "exclusiveMinimum" in spec: min_ = spec["exclusiveMinimum"] is_min_exclusive = True else: min_ = default_min is_min_exclusive = is_default_min_exclusive if "maximum" in spec: max_ = spec["maximum"] is_max_exclusive = False elif "exclusiveMaximum" in spec: max_ = spec["exclusiveMaximum"] is_max_exclusive = True else: max_ = default_max is_max_exclusive = is_default_max_exclusive if min_ > max_: raise ObjectGenerationError("minimum can't be greater than maximum") elif min_ == max_ and (is_max_exclusive or is_min_exclusive): raise ObjectGenerationError( "In an open or half-open interval, minimum must be strictly " "less than maximum") return min_, is_min_exclusive, max_, is_max_exclusive