# frozen_string_literal: true module JSI # a Set of JSI Schemas. always frozen. # # any schema instance is described by a set of schemas. class SchemaSet < ::Set class << self # builds a SchemaSet from a mutable Set which is added to by the given block # # @yield [Set] a Set to which the block may add schemas # @return [SchemaSet] def build mutable_set = Set.new yield mutable_set new(mutable_set) end # ensures the given param becomes a SchemaSet. returns the param if it is already SchemaSet, otherwise # initializes a SchemaSet from it. # # @param schemas [SchemaSet, Enumerable] the object to ensure becomes a SchemaSet # @return [SchemaSet] the given SchemaSet, or a SchemaSet initialized from the given Enumerable # @raise [ArgumentError] when the schemas param is not an Enumerable # @raise [Schema::NotASchemaError] when the schemas param contains objects which are not Schemas def ensure_schema_set(schemas) if schemas.is_a?(SchemaSet) schemas else new(schemas) end end end # initializes a SchemaSet from the given enum and freezes it. # # if a block is given, each element of the enum is passed to it, and the result must be a Schema. # if no block is given, the enum must contain only Schemas. # # @param enum [#each] the schemas to be included in the SchemaSet, or items to be passed to the block # @yieldparam yields each element of enum for preprocessing into a Schema # @yieldreturn [JSI::Schema] # @raise [JSI::Schema::NotASchemaError] def initialize(enum, &block) if enum.is_a?(Schema) raise(ArgumentError, [ "#{SchemaSet} initialized with a #{Schema}", "you probably meant to pass that to #{SchemaSet}[]", "or to wrap that schema in a Set or Array for #{SchemaSet}.new", "given: #{enum.pretty_inspect.chomp}", ].join("\n")) end unless enum.is_a?(Enumerable) raise(ArgumentError, "#{SchemaSet} initialized with non-Enumerable: #{enum.pretty_inspect.chomp}") end super not_schemas = reject { |s| s.is_a?(Schema) } if !not_schemas.empty? raise(Schema::NotASchemaError, [ "#{SchemaSet} initialized with non-schema objects:", *not_schemas.map { |ns| ns.pretty_inspect.chomp }, ].join("\n")) end freeze end # Instantiates a new JSI whose content comes from the given `instance` param. # This SchemaSet indicates the schemas of the JSI - its schemas are inplace # applicators of this set's schemas which apply to the given instance. # # @param instance [Object] the instance to be represented as a JSI # @param uri [#to_str, Addressable::URI] The retrieval URI of the instance. # # It is rare that this needs to be specified, and only useful for instances which contain schemas. # See {Schema::DescribesSchema#new_schema}'s `uri` param documentation. # @param register [Boolean] Whether schema resources in the instantiated JSI will be registered # in the schema registry indicated by param `schema_registry`. # This is only useful when the JSI is a schema or contains schemas. # The JSI's root will be registered with the `uri` param, if specified, whether or not the # root is a schema. # @param schema_registry [SchemaRegistry, nil] The registry to use for references to other schemas and, # depending on `register` and `uri` params, to register this JSI and/or any contained schemas with # declared URIs. # @param stringify_symbol_keys [Boolean] Whether the instance content will have any Symbol keys of Hashes # replaced with Strings (recursively through the document). # Replacement is done on a copy; the given instance is not modified. # @return [JSI::Base subclass] a JSI whose content comes from the given instance and whose schemas are # inplace applicators of the schemas in this set. def new_jsi(instance, uri: nil, register: false, schema_registry: JSI.schema_registry, stringify_symbol_keys: false ) if stringify_symbol_keys instance = Util.deep_stringify_symbol_keys(instance) end applied_schemas = inplace_applicator_schemas(instance) if uri unless uri.respond_to?(:to_str) raise(TypeError, "uri must be string or Addressable::URI; got: #{uri.inspect}") end uri = Util.uri(uri) unless uri.absolute? && !uri.fragment raise(ArgumentError, "uri must be an absolute URI with no fragment; got: #{uri.inspect}") end end jsi_class = JSI::SchemaClasses.class_for_schemas(applied_schemas, includes: SchemaClasses.includes_for(instance), ) jsi = jsi_class.new(instance, jsi_indicated_schemas: self, jsi_schema_base_uri: uri, jsi_schema_registry: schema_registry, ) if register && schema_registry schema_registry.register(jsi) end jsi end # a set of inplace applicator schemas of each schema in this set which apply to the given instance. # (see {Schema#inplace_applicator_schemas}) # # @param instance (see Schema#inplace_applicator_schemas) # @return [JSI::SchemaSet] def inplace_applicator_schemas(instance) SchemaSet.new(each_inplace_applicator_schema(instance)) end # yields each inplace applicator schema which applies to the given instance. # # @param instance (see Schema#inplace_applicator_schemas) # @yield [JSI::Schema] # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil def each_inplace_applicator_schema(instance, &block) return to_enum(__method__, instance) unless block each do |schema| schema.each_inplace_applicator_schema(instance, &block) end nil end # a set of child applicator subschemas of each schema in this set which apply to the child # of the given instance on the given token. # (see {Schema#child_applicator_schemas}) # # @param instance (see Schema#child_applicator_schemas) # @return [JSI::SchemaSet] def child_applicator_schemas(token, instance) SchemaSet.new(each_child_applicator_schema(token, instance)) end # yields each child applicator schema which applies to the child of # the given instance on the given token. # # @param (see Schema#child_applicator_schemas) # @yield [JSI::Schema] # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil def each_child_applicator_schema(token, instance, &block) return to_enum(__method__, token, instance) unless block each do |schema| schema.each_child_applicator_schema(token, instance, &block) end nil end # validates the given instance against our schemas # # @param instance [Object] the instance to validate against our schemas # @return [JSI::Validation::Result] def instance_validate(instance) results = map { |schema| schema.instance_validate(instance) } results.inject(Validation::FullResult.new, &:merge).freeze end # whether the given instance is valid against our schemas # @param instance [Object] the instance to validate against our schemas # @return [Boolean] def instance_valid?(instance) all? { |schema| schema.instance_valid?(instance) } end # @return [String] def inspect -"#{self.class}[#{map(&:inspect).join(", ")}]" end alias_method :to_s, :inspect def pretty_print(q) q.text self.class.to_s q.text '[' q.group_sub { q.nest(2) { q.breakable('') q.seplist(self, nil, :each) { |e| q.pp e } } } q.breakable '' q.text ']' end end end