Class: TableTennis::Util::MagicOptions

Inherits:
Object
  • Object
show all
Defined in:
lib/table_tennis/util/magic_options.rb

Direct Known Subclasses

Config

Constant Summary collapse

MAGIC_ALIASES =
{
  boolean: :bool,
  booleans: :bools,
  bool: :bool,
  bools: :bools,
  float: Float,
  floats: :floats,
  int: Integer,
  integer: Integer,
  integers: :ints,
  ints: :ints,
  lambda: Proc,
  num: Numeric,
  number: Numeric,
  numbers: :nums,
  nums: :nums,
  proc: Proc,
  str: String,
  string: String,
  strings: :strs,
  strs: :strs,
  sym: Symbol,
  symbol: Symbol,
  symbols: :syms,
  syms: :syms,
}
MAGIC_PRETTY =
{
  :bool => "boolean",
  Float => "float",
  Integer => "integer",
  Numeric => "number",
  String => "string",
  Symbol => "symbol",
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema, options = {}) {|_self| ... } ⇒ MagicOptions

Returns a new instance of MagicOptions.

Yields:

  • (_self)

Yield Parameters:



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/table_tennis/util/magic_options.rb', line 56

def initialize(schema, options = {}, &block)
  @magic_attributes, @magic_values = {}, {}

  if self.class == MagicOptions # rubocop:disable Style/ClassEqualityComparison
    raise ArgumentError, "MagicOptions is an abstract class"
  end

  schema.each { magic_add_attribute(_1, _2) }
  update!(options) if options
  yield self if block_given?
end

Instance Attribute Details

#magic_attributesObject

public api (also see [] and []=)



54
55
56
# File 'lib/table_tennis/util/magic_options.rb', line 54

def magic_attributes
  @magic_attributes
end

#magic_valuesObject

public api (also see [] and []=)



54
55
56
# File 'lib/table_tennis/util/magic_options.rb', line 54

def magic_values
  @magic_values
end

Class Method Details

.magic_coerce(value, type) ⇒ Object

coerce value into type. pretty conservative at the moment



221
222
223
224
225
226
227
228
229
# File 'lib/table_tennis/util/magic_options.rb', line 221

def self.magic_coerce(value, type)
  if type == :bool
    case value
    when true, 1, "1", "true" then value = true
    when false, 0, "", "0", "false" then value = false
    end
  end
  value
end

.magic_is_a?(value, klass) ⇒ Boolean

like is_a?, but supports :bool and allows ints to be floats

Returns:

  • (Boolean)


232
233
234
235
236
237
238
239
240
# File 'lib/table_tennis/util/magic_options.rb', line 232

def self.magic_is_a?(value, klass)
  if klass == :bool
    value == true || value == false
  elsif klass == Float
    value.is_a?(klass) || value.is_a?(Integer)
  else
    value.is_a?(klass)
  end
end

.magic_pretty(klass) ⇒ Object

pretty print a class (or :bool)



279
# File 'lib/table_tennis/util/magic_options.rb', line 279

def self.magic_pretty(klass) = MAGIC_PRETTY[klass] || klass.to_s

.magic_resolve(type) ⇒ Object

resolve :boolean to :bool, :int => Integer class, etc.



282
# File 'lib/table_tennis/util/magic_options.rb', line 282

def self.magic_resolve(type) = MAGIC_ALIASES[type] || type

.magic_validate!(name, value, type) ⇒ Object

validate name=value against type, raise on failure



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/table_tennis/util/magic_options.rb', line 151

def self.magic_validate!(name, value, type)
  # we validate against coerced values, but squirrel away the original
  # uncoerced in case we need to use it inside an error message
  original, value = value, magic_coerce(value, type)
  return if value.nil?

  case type
  when Array then validate_any_of(value, type)
  when Class, :bool then validate_class(value, type)
  when Hash then validate_hash(value, type)
  when Proc then type.call(value)
  when Range then validate_range(value, type)
  when Regexp then validate_regexp(value, type)
  when :bools, :floats, :ints, :nums, :strs, :syms then validate_array(value, type)
  else
    raise "impossible"
  end
  value
rescue ArgumentError => ex
  # add context to msg if necessary
  msg = ex.message
  if !msg.include?("#{name} = #{original.inspect}")
    msg = "#{self}.#{name} = #{original.inspect} failed, #{msg}"
  end
  raise ArgumentError, msg
end

.validate_any_of(value, possibilities) ⇒ Object

validators



182
183
184
185
186
# File 'lib/table_tennis/util/magic_options.rb', line 182

def self.validate_any_of(value, possibilities)
  if !possibilities.include?(value)
    raise ArgumentError, "expected one of #{possibilities.inspect}"
  end
end

.validate_array(value, array_type) ⇒ Object



213
214
215
216
217
218
# File 'lib/table_tennis/util/magic_options.rb', line 213

def self.validate_array(value, array_type)
  klass = magic_resolve(array_type.to_s[..-2].to_sym)
  if !(value.is_a?(Array) && value.all? { magic_is_a?(_1, klass) })
    raise ArgumentError, "expected array of #{array_type}"
  end
end

.validate_class(value, klass) ⇒ Object



188
189
190
191
192
# File 'lib/table_tennis/util/magic_options.rb', line 188

def self.validate_class(value, klass)
  if !magic_is_a?(value, klass)
    raise ArgumentError, "expected #{magic_pretty(klass)}"
  end
end

.validate_hash(value, hash_type) ⇒ Object



194
195
196
197
198
199
# File 'lib/table_tennis/util/magic_options.rb', line 194

def self.validate_hash(value, hash_type)
  kk, vk = hash_type.first
  if !(value.is_a?(Hash) && value.all? { magic_is_a?(_1, kk) && magic_is_a?(_2, vk) })
    raise ArgumentError, "expected hash of #{magic_pretty(kk)} => #{magic_pretty(vk)}"
  end
end

.validate_range(value, range) ⇒ Object



201
202
203
204
205
# File 'lib/table_tennis/util/magic_options.rb', line 201

def self.validate_range(value, range)
  if !value.is_a?(Numeric) || !range.include?(value)
    raise ArgumentError, "expected to be in range #{range.inspect}"
  end
end

.validate_regexp(value, regexp) ⇒ Object



207
208
209
210
211
# File 'lib/table_tennis/util/magic_options.rb', line 207

def self.validate_regexp(value, regexp)
  if !value.is_a?(String) || !value.match?(regexp)
    raise ArgumentError, "expected to be a string matching #{regexp}"
  end
end

Instance Method Details

#inspectObject



71
72
73
74
# File 'lib/table_tennis/util/magic_options.rb', line 71

def inspect
  values = magic_values.compact.map { "#{_1}=#{_2.inspect}" }.join(", ")
  "#<#{self.class} #{values}>"
end

#magic_add_attribute(name, type) ⇒ Object

magic_add_attribute



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
# File 'lib/table_tennis/util/magic_options.rb', line 80

def magic_add_attribute(name, type)
  # resolve :boolean to :bool, :int => Integer class, etc.
  type = if type.is_a?(Hash)
    type.to_h { [self.class.magic_resolve(_1), self.class.magic_resolve(_2)] }
  else
    self.class.magic_resolve(type)
  end

  # sanity check for schema errors
  if (error = magic_sanity(name, type))
    raise ArgumentError, "MagicOptions schema #{name.inspect} #{error}"
  end

  # all is well
  magic_attributes[name] = type
  if !respond_to?(name)
    define_singleton_method(name) { self[name] }
  end
  if type == :bool && !respond_to?("#{name}?")
    define_singleton_method("#{name}?") { !!self[name] }
  end
  if !respond_to?("#{name}=")
    define_singleton_method("#{name}=") { |value| self[name] = value }
  end
end

#magic_get(name) ⇒ Object Also known as: []

magic_get/set

Raises:

  • (ArgumentError)


132
133
134
135
# File 'lib/table_tennis/util/magic_options.rb', line 132

def magic_get(name)
  raise ArgumentError, "unknown #{self.class}.#{name}" if !magic_attributes.key?(name)
  magic_values[name]
end

#magic_sanity(name, type) ⇒ Object

sanity check a name/type from the schema



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/table_tennis/util/magic_options.rb', line 107

def magic_sanity(name, type)
  if !name.is_a?(Symbol)
    return "attribute names must be symbols"
  end
  if !name.to_s.match?(/\A[a-z_][0-9a-z_]*\z/i)
    return "attribute names must be valid method names"
  end

  case type
  when :bool, :bools, :floats, :ints, :nums, :strs, :syms, Class, Proc, Range, Regexp
    return
  when Array
    "must be an array of possible values" if type.empty?
  when Hash
    valid = type.length == 1 && type.first.all? { _1 == :bool || _1.is_a?(Class) }
    "must be { class => class }" if !valid
  else
    "unknown schema type #{type.inspect}"
  end
end

#magic_set(name, value) ⇒ Object Also known as: []=

Raises:

  • (ArgumentError)


137
138
139
140
# File 'lib/table_tennis/util/magic_options.rb', line 137

def magic_set(name, value)
  raise ArgumentError, "unknown #{self.class}.#{name}=" if !magic_attributes.key?(name)
  magic_values[name] = self.class.magic_validate!(name, value, magic_attributes[name])
end

#to_hObject



69
# File 'lib/table_tennis/util/magic_options.rb', line 69

def to_h = magic_values.dup

#update!(hash) ⇒ Object



68
# File 'lib/table_tennis/util/magic_options.rb', line 68

def update!(hash) = hash.each { send("#{_1}=", _2) }