A Literal Ruby Gem [WIP]
Types
Literal uses Ruby-native types. Any method that responds to ===(value)
is considered a type. Note, this is how Ruby’s case statements and pattern matching work. It’s also how Array#any?
and Array#all?
work. Essentially all Ruby objects are types and just work the way you’d expect them to. A few examples:
- On a
Range
,===(value)
checks if the value is within the range. - On a
Regexp
,===(value)
checks if the value matches the pattern. - On a
Class
,===(value)
checks if the value is an instance of the class. - On a
Proc
,===(value)
calls the proc with the value. - On a
String
,===(value)
checks if the value is equal to the string. - On the class
String
,===(value)
checks if the value is a string.
Literal extends this idea with the concept of generics or parameterised types. A generic is a method that returns an object that respond to ===(value)
.
If we want to check that a given value is an Array
, we could do this:
Array === [1, 2, 3]
But what if we want to check that it’s an Array of Integers? Literal provides a library of special types that can be composed for this purpose. In this case, we can use the type _Array(Integer)
.
_Array(Integer) === [1, 2, 3]
These special types are defined on the Literal::Types
module. To access them in a class, you can extend
this module. To access them on an instance, you can include
the module. If you want to use them globally, you can extend
the module at the root.
extend Literal::Types
This is recommended for applications, but not for libraries, as we don’t want to pollute the global namespace from library code.
Literal::Properties
, Literal::Object
, Literal::Struct
and Literal::Data
already extend Literal::Types
, so you don’t need to extend Literal::Types
yourself if you’re only using literal types for literal properties.
Properties
Literal::Properties
is a mixin that allows you to define the structure of an object. Properties are defined using the prop
method.
The first argument is the name of the property as a Symbol
. The second argument is the type of the property. Remember, the type can be any object that responds to ===(value)
.
The third argument is optional. You can set this to :*
, :**
, :&
, or :positional
to change the kind of initializer parameter.
class Person
extend Literal::Properties
prop :name, String
prop :age, Integer
end
You can also use keyword arguments to define readers and writers. These can be set to false
, :public
, :protected
, or :private
and default to false
.
class Person
extend Literal::Properties
prop :name, String, reader: :public
prop :age, Integer, writer: :protected
end
Properties are required by deafult. To make them optional, set the type to a that responds to ===(nil)
with true
. Literal::Types
provides a special types for this purpose. Let’s make the age optional by setting its type to a _Nilable(Integer)
:
class Person
extend Literal::Properties
prop :name, String
prop :age, _Nilable(Integer)
end
Alternatively, you can give the property a default value. This default value must match the type of the property.
class Person
extend Literal::Properties
prop :name, String, default: "John Doe"
prop :age, _Nilable(Integer)
end
Note, the above example will fail unless you have frozen string literals enabled. (Which, honestly, you should.) Default values must be frozen. If you can’t use a frozen value, you can pass a proc instead.
class Person
extend Literal::Properties
prop :name, String, default: -> { "John Doe" }
prop :age, _Nilable(Integer)
end
The proc will be called to generate the default value.
You can also pass a block to the prop
method. This block will be called with the value of the property when it’s set, which is useful for coercion.
class Person
extend Literal::Properties
prop :name, String
prop :age, Integer do |value|
value.to_i
end
end
Coercion takes place prior to type-checking, so you can safely coerce a value to a different type in the block.
You can use properties that conflict with ruby keywords. Literal will handle everything for you automatically.
class Person
extend Literal::Properties
prop :class, String, :positional
prop :end, Integer
end
If you’d prefer to subclass than extend a module, you can use the Literal::Object
class instead. Literal::Object
literally extends Literal::Properties
.
class Person < Literal::Object
prop :name, String
prop :age, Integer
end
Structs
Literal::Struct
is like Literal::Object
, but it also provides a few extras.
Structs implement ==
so you can compare one struct to another. They also implement hash
. Structs also have public readers and writers by default.
class Person < Literal::Struct
prop :name, String
prop :age, Integer
end
Data
Literal::Data
is like Literal::Struct
, but you can’t define writers. Additionally, objects are frozen after initialization. Additionally any non-frozen properties are duplicated and frozen.
class Person < Literal::Data
prop :name, String
prop :age, Integer
end