# typed: strict module DataModel # Provide Error building functionality as a mixin module Errors include Kernel extend T::Sig TTemporal = T.type_alias { T.any(::Date, ::Time, ::DateTime) } ## Constructors # Type error applies when a value is not of the expected type sig { params(cls: T.class_of(Object), value: Object).returns(TError) } def type_error(cls, value) [:type, [cls, value]] end # Coerce error applies when a value cannot be coerced to the expected type sig { params(cls: T.class_of(Object), value: Object).returns(TError) } def coerce_error(cls, value) [:coerce, [cls, value]] end # Missing error applies when a value is missing sig { params(cls: T.class_of(Object)).returns(TError) } def missing_error(cls) [:missing, cls] end # Inclusion error applies when a value is not in a set of allowed values sig { params(set: T::Array[T.any(Symbol, String)]).returns(TError) } def inclusion_error(set) [:inclusion, set] end # Exclusive error applies when a value is in a set of disallowed values sig { params(set: T::Array[T.any(Symbol, String)]).returns(TError) } def exclusion_error(set) [:exclusion, set] end # Blank error applies when a value is blank sig { returns(TError) } def blank_error [:blank, nil] end # Extra keys error applies when a hash has extra keys sig { params(keys: T::Array[Symbol]).returns(TError) } def extra_keys_error(keys) [:extra_keys, keys] end # Min applies when value is less then the minimum sig { params(min: Numeric, val: Numeric).returns(TError) } def min_error(min, val) [:min, [min, val]] end # Max applies when value is less then the minimum sig { params(min: Numeric, val: Numeric).returns(TError) } def max_error(min, val) [:max, [min, val]] end # Earliest applies when value is earlier then earliest sig { params(earliest: TTemporal, val: TTemporal).returns(TError) } def earliest_error(earliest, val) [:earliest, [earliest, val]] end # Latest applies when value is earlier then earliest sig { params(latest: TTemporal, val: TTemporal).returns(TError) } def latest_error(latest, val) [:latest, [latest, val]] end # Format applies when value does not match a format sig { params(format: Object, val: String).returns(TError) } def format_error(format, val) [:format, [format, val]] end ## Messages # Generate a message for a type error sig { params(cls: T.class_of(Object), value: Object).returns(String) } def type_error_message(cls, value) "#{value.inspect} is not a #{cls.name}, it is a #{value.class.name}" end # Generate a message for a coerce error sig { params(cls: T.class_of(Object), value: Object).returns(String) } def coerce_error_message(cls, value) "cannot be coerced to #{cls.name}, it is a #{value.class.name}" end # Generate a message for a missing error sig { params(cls: T.class_of(Object)).returns(String) } def missing_error_message(cls) "missing value, expected a #{cls.name}" end # Generate a message for an inclusion error sig { params(set: T::Array[Symbol]).returns(String) } def inclusion_error_message(set) "must be one of #{set.join(', ')}" end # Generate a message for an exclusion error sig { params(set: T::Array[Symbol]).returns(String) } def exclusion_error_message(set) "must not be one of #{set.join(', ')}" end # Generate a message for a blank error sig { returns(String) } def blank_error_message "cannot be blank" end # Generate a message for an extra keys error sig { params(keys: T::Array[Symbol]).returns(String) } def extra_keys_error_message(keys) "more elements found in closed hash then specified children: #{keys.join(', ')}" end # Generate a message for a min error sig { params(min: Numeric, val: Numeric).returns(String) } def min_error_message(min, val) "value is less than the minimum of #{min}, it is #{val}" end # Generate a message for a min error sig { params(max: Numeric, val: Numeric).returns(String) } def max_error_message(max, val) "value is more than the maximum of #{max}, it is #{val}" end # Generate a message for a value that occurs earlier then the specified earliest point sig { params(earliest: TTemporal, val: TTemporal).returns(String) } def early_error_message(earliest, val) "value #{val} is before #{earliest}" end # Generate a message for a value that occurs later then the specified latest point sig { params(latest: TTemporal, val: TTemporal).returns(String) } def late_error_message(latest, val) "value #{val} is after #{latest}" end # Generate a message for a value that does not match the format sig { params(format: Object, val: String).returns(String) } def format_error_message(format, val) "value #{val} does not match format #{format}" end ## API # TODO: split this file TErrorMessageBuilder = T.type_alias { T.proc.params(ctx: T.untyped).returns(String) } # Register a custom error message for use with custom errors sig { params(type: Symbol, block: TErrorMessageBuilder).void } def register_error_message(type, &block) error_message_builders[type] = block end TErrorMessages = T.type_alias { T::Hash[Symbol, TErrorMessageBuilder] } TClassValueCtx = T.type_alias { [T.class_of(Object), Object] } TClassCtx = T.type_alias { T.class_of(Object) } TSetCtx = T.type_alias { T::Array[Symbol] } TWithinCtx = T.type_alias { [Numeric, Numeric] } TWithinTemporalCtx = T.type_alias { [TTemporal, TTemporal] } TFormatCtx = T.type_alias { [Object, String] } # Get the error message builders sig { returns(TErrorMessages) } def error_message_builders if @error_messages.nil? @error_messages ||= T.let({}, T.nilable(TErrorMessages)) # wire up defaults register_error_message(:type) do |ctx| cls, val = T.let(ctx, TClassValueCtx) type_error_message(cls, val) end register_error_message(:coerce) do |ctx| cls, val = T.let(ctx, TClassValueCtx) coerce_error_message(cls, val) end register_error_message(:missing) do |ctx| cls = T.let(ctx, TClassCtx) missing_error_message(cls) end register_error_message(:inclusion) do |ctx| set = T.let(ctx, TSetCtx) inclusion_error_message(set) end register_error_message(:exclusion) do |ctx| set = T.let(ctx, TSetCtx) exclusion_error_message(set) end register_error_message(:extra_keys) do |ctx| set = T.let(ctx, TSetCtx) extra_keys_error_message(set) end register_error_message(:min) do |ctx| min, val = T.let(ctx, TWithinCtx) min_error_message(min, val) end register_error_message(:max) do |ctx| max, val = T.let(ctx, TWithinCtx) max_error_message(max, val) end register_error_message(:earliest) do |ctx| earliest, val = T.let(ctx, TWithinTemporalCtx) early_error_message(earliest, val) end register_error_message(:latest) do |ctx| latest, val = T.let(ctx, TWithinTemporalCtx) late_error_message(latest, val) end register_error_message(:blank) do blank_error_message end register_error_message(:format) do |ctx| format, val = T.let(ctx, TFormatCtx) format_error_message(format, val) end end @error_messages end # Build the error message for a given error sig { params(error: TError).returns(String) } def error_message(error) type = T.let(error[0], Symbol) ctx = T.let(error[1], T.untyped) builder = error_message_builders[type] if builder.nil? raise "no error message builder for #{type}" end builder.call(ctx) end # TODO: separate builders from other use cases for this mixin # Build error messages from error object sig { params(error: Error).returns(T::Hash[Symbol, T::Array[String]]) } def error_messages(error) error.to_h.transform_values do |error_list| error_list.map { |e| error_message(e) } end end sig { params(error: Error, from: T.class_of(Object), to: T.class_of(Object)).void } def set_error_class(error, from, to) error.transform_context do |ctx, type| case type when :type, :coerce cls, val = T.cast(ctx, TClassValueCtx) if cls == from [to, val] else [cls, val] end when :missing if ctx == from [to, val] else [cls, val] end else [cls, val] end end end end end