# typed: strict # Scan a schema into a struct that can be inspected to construct a model validator # # schema eg: # [:string, { min: 1, max: 10}] # [:tuple, { title: "coordinates" }, :double, :double] # [:hash, { open: false }, # [:first_name, :string] # [:last_name, :string]] # # first param is type, which is a key lookup in the registry # second param is args, this is optional, but is a way to configure a type # rest are type params. these are used to configure a type at the point of instantiation. Think of them as generics. # # params are either # symbol, for example tuple types # array, for object types to configure child properties. module DataModel module Scanner include Kernel include Logging extend T::Sig extend self class Node < T::Struct prop :type, Symbol, default: :nothing prop :args, T::Hash[Symbol, Object], default: {} prop :params, T::Array[Object], default: [] end # Scan a schema, which is defined as a data structure, into a struct that is easier to work with. # "Syntax" validations will be enforced at this level. sig { params(schema: TSchema, registry: DataModel::Registry).returns(Node) } def scan(schema, registry = Registry.instance) # state: # nil (start) -> :type (we have a type) -> :args (we have arguments) scanned = Node.new state = T.let(nil, T.nilable(Symbol)) log.debug("scanning schema: #{schema.inspect}") for pos in (0...schema.length) token = schema[pos] dbg = "pos: #{pos}, token: #{token.inspect}, state: #{state.inspect}" log.debug(dbg) # detect optional args missing if !token.is_a?(Hash) && state == :type log.debug("detected optional args missing at (#{dbg}), moving state to :args") # move state forward state = :args end # we are just collecting params at this point if state == :args if !token.is_a?(Array) && !token.is_a?(Symbol) raise "expected type params at (#{dbg}), which should be either a symbol or an array" end scanned.params << token log.debug("collecting params at (#{dbg})") next end # we can determine meaning based on type and state case token when Symbol if !state.nil? raise "got a symbol at(#{dbg}), but validator already defined" end if !registry.type?(token) # TODO: need a much better error here, this is what people see when registration is not there raise "expected a type in (#{dbg}), but found #{token.inspect} which is not a registered type" end scanned.type = token state = :type log.debug("got a symbol, determined token is a type at (#{dbg}), moving state to :type") when Hash if state != :type raise "got a hash at (#{dbg}), but state is not :type (#{state.inspect})" end scanned.args = token state = :args log.debug("got a hash, determined token is args at (#{dbg}), moving state to :args") else raise "got token #{token.inspect} at (#{dbg}) which was unexpected given the scanner was in a state of #{state}" end end return scanned end end end