Class: Blueticks::Types::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/blueticks/types/base.rb

Overview

Lightweight DTO base class. Subclasses declare fields with ‘field` + a type spec. Provides `.from_hash(raw)` that asserts shape and raises ValidationError on mismatch.

Type specs supported:

:string, :integer, :number, :boolean, :hash, :any
[:array, inner_spec]            — array of inner_spec values
[:enum, [..values..]]           — string in allowed set
[:hash_of, value_spec]          — Hash<String, value_spec>
SomeTypeClass                    — nested DTO; calls .from_hash on it

Mark fields nullable: true to permit nil. Mark optional: true to permit missing.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(values) ⇒ Base

Returns a new instance of Base.



138
139
140
# File 'lib/blueticks/types/base.rb', line 138

def initialize(values)
  values.each { |k, v| instance_variable_set("@#{k}", v) }
end

Class Method Details

.assert_kind!(value, expected, path, label) ⇒ Object



129
130
131
132
133
134
135
# File 'lib/blueticks/types/base.rb', line 129

def assert_kind!(value, expected, path, label)
  return if value.is_a?(expected)

  raise Errors::ValidationError.new(
    message: "Field '#{path}' must be #{label}, got #{value.class}"
  )
end

.coerce(value, type, path) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/blueticks/types/base.rb', line 68

def coerce(value, type, path)
  case type
  when :string
    assert_kind!(value, String, path, "string")
    value
  when :integer
    unless value.is_a?(Integer) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
      raise Errors::ValidationError.new(
        message: "Field '#{path}' must be integer, got #{value.class}"
      )
    end
    value
  when :number
    unless value.is_a?(Numeric) && !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
      raise Errors::ValidationError.new(
        message: "Field '#{path}' must be number, got #{value.class}"
      )
    end
    value
  when :boolean
    unless [true, false].include?(value)
      raise Errors::ValidationError.new(
        message: "Field '#{path}' must be boolean, got #{value.class}"
      )
    end
    value
  when :hash
    assert_kind!(value, Hash, path, "hash")
    value
  when :any
    value
  when Array
    head = type[0]
    case head
    when :array
      assert_kind!(value, Array, path, "array")
      value.each_with_index.map { |item, i| coerce(item, type[1], "#{path}[#{i}]") }
    when :enum
      allowed = type[1]
      unless allowed.include?(value)
        raise Errors::ValidationError.new(
          message: "Field '#{path}' must be one of #{allowed.inspect}, got #{value.inspect}"
        )
      end
      value
    when :hash_of
      assert_kind!(value, Hash, path, "hash")
      out = {}
      value.each { |k, v| out[k.to_s] = coerce(v, type[1], "#{path}.#{k}") }
      out
    else
      raise ArgumentError, "Unknown compound type spec: #{type.inspect}"
    end
  else
    raise ArgumentError, "Unknown type spec: #{type.inspect}" unless type.is_a?(Class) && type < Base

    type.from_hash(value, context: "#{path} (#{type.name})")

  end
end

.field(name, type, nullable: false, optional: false, wire_name: nil) ⇒ Object



24
25
26
27
28
# File 'lib/blueticks/types/base.rb', line 24

def field(name, type, nullable: false, optional: false, wire_name: nil)
  wire = (wire_name || name).to_s
  fields[name] = { type: type, nullable: nullable, optional: optional, wire: wire }
  attr_reader name
end

.fieldsObject



20
21
22
# File 'lib/blueticks/types/base.rb', line 20

def fields
  @fields ||= {}
end

.from_hash(raw, context: name) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/blueticks/types/base.rb', line 30

def from_hash(raw, context: name)
  unless raw.is_a?(Hash)
    raise Errors::ValidationError.new(
      message: "Expected Hash for #{context}, got #{raw.class}"
    )
  end

  values = {}
  fields.each do |attr, spec|
    wire = spec[:wire]
    present = raw.key?(wire)
    unless present
      if spec[:optional] || spec[:nullable]
        values[attr] = nil
        next
      end
      raise Errors::ValidationError.new(
        message: "Missing required field '#{wire}' in #{context}"
      )
    end

    v = raw[wire]
    if v.nil?
      if spec[:nullable] || spec[:optional]
        values[attr] = nil
        next
      end
      raise Errors::ValidationError.new(
        message: "Field '#{wire}' may not be null in #{context}"
      )
    end

    values[attr] = coerce(v, spec[:type], "#{context}.#{wire}")
  end

  new(values)
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



148
149
150
# File 'lib/blueticks/types/base.rb', line 148

def ==(other)
  other.is_a?(self.class) && to_h == other.to_h
end

#hashObject



153
154
155
# File 'lib/blueticks/types/base.rb', line 153

def hash
  to_h.hash
end

#to_hObject



142
143
144
145
146
# File 'lib/blueticks/types/base.rb', line 142

def to_h
  out = {}
  self.class.fields.each_key { |k| out[k] = instance_variable_get("@#{k}") }
  out
end