Runtime assertions for Ruby literal.fun
ruby
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Value objects (#293)

This PR introduces `Literal::Value` objects. Literal::Values are
designed to wrap primitives (though they can wrap any object) in a
branded object allowing for more precise type-matching.

```ruby
UserID = Literal::Value(Integer)

my_user_id = UserID.new(123)
```

### Constraints

You can pass constraints, same as `_Constraint`

```ruby
Username = Literal::Value(String, length: 3..15)
```

### Coercion

Literal value classes can be coerced into a coercion by default

```ruby
prop :user, UserID, &UserID
```

### Delegation

You can delegate to underlying methods

```ruby
Username = Literal::Value(String) do
delegate :length
end
```

Literal will default to delegating to basic coercion methods. For
example, if your value is a String, it will delegate `to_s` and `to_str`
to the underlying value by default. Otherwise, delegation is explicit.

Only delegate what you need. Constraining the interface allows for more
flexibility later on. If you want to replace a value object with a
regular object, you won’t need to re-implement the entire interface of a
primitive.

### Customisation

```ruby
Name = Literal::Value(String) do
def first
@value.split(" ").first
end
end
```

### Freezing

Literal value classes and literal value objects are frozen by default,
though their underlying values are not automatically frozen.

authored by

Joel Drapper and committed by
GitHub
34de62d8 1ec5382f

+150 -1
+36 -1
lib/literal.rb
··· 11 11 12 12 loader.collapse("#{__dir__}/literal/flags") 13 13 loader.collapse("#{__dir__}/literal/errors") 14 + loader.setup 15 + end 14 16 15 - loader.setup 17 + def self.Value(*args, **kwargs, &block) 18 + value_class = Class.new(Literal::Value) 19 + 20 + type = Literal::Types._Constraint(*args, **kwargs) 21 + value_class.define_method(:type) { type } 22 + 23 + if subtype?(type, of: Integer) 24 + value_class.alias_method :to_i, :value 25 + elsif subtype?(type, of: String) 26 + value_class.alias_method :to_s, :value 27 + value_class.alias_method :to_str, :value 28 + elsif subtype?(type, of: Array) 29 + value_class.alias_method :to_a, :value 30 + value_class.alias_method :to_ary, :value 31 + elsif subtype?(type, of: Hash) 32 + value_class.alias_method :to_h, :value 33 + elsif subtype?(type, of: Float) 34 + value_class.alias_method :to_f, :value 35 + elsif subtype?(type, of: Set) 36 + value_class.alias_method :to_set, :value 37 + end 38 + 39 + value_class.class_eval(&block) if block 40 + value_class.freeze 41 + end 42 + 43 + def self.Delegator(*args, **kwargs, &block) 44 + delegator_class = Class.new(Literal::Delegator) 45 + 46 + type = Literal::Types._Constraint(*args, **kwargs) 47 + delegator_class.define_method(:__type__) { type } 48 + 49 + delegator_class.class_eval(&block) if block 50 + delegator_class.freeze 16 51 end 17 52 18 53 def self.Enum(type)
+25
lib/literal/delegator.rb
··· 1 + # frozen_string_literal: true 2 + 3 + class Literal::Delegator < SimpleDelegator 4 + def self.to_proc 5 + -> (value) { new(value) } 6 + end 7 + 8 + def self.[](value) 9 + new(value) 10 + end 11 + 12 + def initialize(value) 13 + Literal.check(expected: __type__, actual: value) 14 + super 15 + freeze 16 + end 17 + 18 + def ===(other) 19 + self.class === other && __getobj__ == other.__getobj__ 20 + end 21 + 22 + alias_method :==, :=== 23 + 24 + freeze 25 + end
+43
lib/literal/value.rb
··· 1 + # frozen_string_literal: true 2 + 3 + class Literal::Value 4 + def self.to_proc 5 + -> (value) { new(value) } 6 + end 7 + 8 + def self.[](value) 9 + new(value) 10 + end 11 + 12 + def self.delegate(*methods) 13 + methods.each do |method_name| 14 + class_eval(<<~RUBY, __FILE__, __LINE__ + 1) 15 + # frozen_string_literal: true 16 + 17 + def #{method_name}(...) 18 + @value.#{method_name}(...) 19 + end 20 + RUBY 21 + end 22 + end 23 + 24 + def initialize(value) 25 + Literal.check(expected: type, actual: value) 26 + @value = value 27 + freeze 28 + end 29 + 30 + attr_reader :value 31 + 32 + def inspect 33 + "#{self.class.name}(#{value.inspect})" 34 + end 35 + 36 + def ===(other) 37 + self.class === other && @value == other.value 38 + end 39 + 40 + alias_method :==, :=== 41 + 42 + freeze 43 + end
+27
test/delegator.test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + UserID = Literal::Delegator(String) do 4 + def double = length * 2 5 + end 6 + 7 + test ".===" do 8 + user_id = UserID.new("123") 9 + assert UserID === user_id 10 + end 11 + 12 + test ".[]" do 13 + user_id = UserID["123"] 14 + assert UserID === user_id 15 + end 16 + 17 + test "custom methods" do 18 + user_id = UserID.new("123") 19 + assert_equal user_id.double, 6 20 + end 21 + 22 + test "#===" do 23 + user_id = UserID.new("123") 24 + 25 + assert user_id === user_id 26 + refute user_id === "123" 27 + end
+19
test/value.test.rb
··· 1 + # frozen_string_literal: true 2 + 3 + UserID = Literal::Value(Integer) 4 + Age = Literal::Value(Integer, 18..) 5 + Name = Literal::Value(String) do 6 + delegate :length 7 + end 8 + 9 + test do 10 + user_id = UserID.new(123) 11 + assert_equal(123, user_id.to_i) 12 + 13 + assert_raises Literal::TypeError do 14 + Age.new(17) 15 + end 16 + 17 + name = Name.new("Joel") 18 + assert_equal 4, name.length 19 + end