README.md in dry-data-0.4.2 vs README.md in dry-data-0.5.0
- old
+ new
@@ -12,12 +12,55 @@
[][gemnasium]
[][codeclimate]
[][codeclimate]
[][inchpages]
-A simple type system for Ruby with support for coercions.
+A simple and extendible type system for Ruby with support for kernel coercions,
+form coercions, sum types, constrained types and default-value types.
+Used by:
+
+* [dry-validation](https://github.com/dryrb/dry-validation) for params coercions
+* [rom-repository](https://github.com/rom-rb/rom-repository) for auto-mapped structs
+* [rom](https://github.com/rom-rb/rom)'s adapters for relation schema definitions
+* your project...?
+
+Articles:
+
+* ["Invalid Object Is An Anti-Pattern"](http://solnic.eu/2015/12/28/invalid-object-is-an-anti-pattern.html)
+
+## dry-data vs virtus
+
+[Virtus](https://github.com/solnic/virtus) has been a successful library, unfortunately
+it is "only" a by-product of an ActiveRecord ORM which carries many issues typical
+to ActiveRecord-like features that we all know from Rails, especially when it
+comes to very complicated coercion logic, mixing unrelated concerns, polluting
+application layer with concerns that should be handled at the bounderies etc.
+
+`dry-data` has been created to become a better tool that solves *similar* (but
+not identical!) problems related to type-safety and coercions. It is a superior
+solution because:
+
+* Types are [categorized](#built-in-type-categories), which is especially important for coercions
+* Types are objects and they are easily reusable
+* Has [structs](#structs) and [values](#values) with *a simple DSL*
+* Has [constrained types](#constrained-types)
+* Has [optional types](#optional-types)
+* Has [defaults](#defaults)
+* Has [sum-types](#sum-types)
+* Has [enums](#enums)
+* Has [hash type with type schemas](#hashes)
+* Has [array type with member type](#arrays)
+* Suitable for many use-cases while remaining simple, in example:
+ * Params coercions
+ * Domain "models"
+ * Defining various domain-specific, shared information using enums or values
+ * Annotating objects
+ * and more...
+* There's no const-missing magic and complicated const lookups like in Virtus
+* AND is roughly 10-12x faster than Virtus
+
## Installation
Add this line to your application's Gemfile:
```ruby
@@ -44,111 +87,143 @@
- `strict` - doesn't coerce and checks the input type against the primitive class
- `coercible` - tries to coerce and raises type-error if it failed
- `form` - non-strict coercion types suitable for form params
- `maybe` - accepts either a nil or something else
+### Configuring Types Module
+
+In `dry-data` a type is an object with a constructor that knows how to handle
+input. On top of that there are high-level types like a sum-type, constrained type,
+optional type or default value type.
+
+To acccess all the built-in type objects you can configure `dry-data` with a
+namespace module:
+
+``` ruby
+module Types
+end
+
+Dry::Data.configure do |config|
+ config.namespace = Types
+end
+
+# after defining your custom types (if you've got any) you can finalize setup
+Dry::Data.finalize
+
+# this defines all types under your namespace, in example:
+Types::Coercible::String
+# => #<Dry::Data::Type:0x007feffb104aa8 @constructor=#<Method: Kernel.String>, @primitive=String>
+```
+
+With types accessible as constants you can easily compose more complex types,
+like sum-types or constrained types, in hash schemas or structs:
+
+``` ruby
+Dry::Data.configure do |config|
+ config.namespace = Types
+end
+
+Dry::Data.finalize
+
+module Types
+ Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
+ Age = Int.constrained(gt: 18)
+end
+
+class User < Dry::Data::Struct
+ attribute :name, Types::String
+ attribute :email, Types::Email
+ attribute :age, Types::Age
+end
+```
+
### Built-in Type Categories
+Assuming you configured types under `Types` module namespace:
+
Non-coercible:
-- `nil`
-- `symbol`
-- `class`
-- `true`
-- `false`
-- `date`
-- `date_time`
-- `time`
+- `Types::Nil`
+- `Types::Symbol`
+- `Types::Class`
+- `Types::True`
+- `Types::False`
+- `Types::Date`
+- `Types::DateTime`
+- `Types::Time`
Coercible types using kernel coercion methods:
-- `coercible.string`
-- `coercible.int`
-- `coercible.float`
-- `coercible.decimal`
-- `coercible.array`
-- `coercible.hash`
+- `Types::Coercible::String`
+- `Types::Coercible::Int`
+- `Types::Coercible::Float`
+- `Types::Coercible::Decimal`
+- `Types::Coercible::Array`
+- `Types::Coercible::Hash`
Optional strict types:
-- `maybe.strict.string`
-- `maybe.strict.int`
-- `maybe.strict.float`
-- `maybe.strict.decimal`
-- `maybe.strict.array`
-- `maybe.strict.hash`
+- `Types::Maybe::Strict::String`
+- `Types::Maybe::Strict::Int`
+- `Types::Maybe::Strict::Float`
+- `Types::Maybe::Strict::Decimal`
+- `Types::Maybe::Strict::Array`
+- `Types::Maybe::Strict::Hash`
Optional coercible types:
-- `maybe.coercible.string`
-- `maybe.coercible.int`
-- `maybe.coercible.float`
-- `maybe.coercible.decimal`
-- `maybe.coercible.array`
-- `maybe.coercible.hash`
+- `Types::Maybe::Coercible::String`
+- `Types::Maybe::Coercible::Int`
+- `Types::Maybe::Coercible::Float`
+- `Types::Maybe::Coercible::Decimal`
+- `Types::Maybe::Coercible::Array`
+- `Types::Maybe::Coercible::Hash`
Coercible types suitable for form param processing:
-- `form.nil`
-- `form.date`
-- `form.date_time`
-- `form.time`
-- `form.true`
-- `form.false`
-- `form.bool`
-- `form.int`
-- `form.float`
-- `form.decimal`
+- `Types::Form::Nil`
+- `Types::Form::Date`
+- `Types::Form::DateTime`
+- `Types::Form::Time`
+- `Types::Form::True`
+- `Types::Form::False`
+- `Types::Form::Bool`
+- `Types::Form::Int`
+- `Types::Form::Float`
+- `Types::Form::Decimal`
-### Accessing Built-in Types
+### Strict vs Coercible Types
``` ruby
-# default passthrough category
-float = Dry::Data["float"]
+Types::Strict::Int[1] # => 1
+Types::Strict::Int['1'] # => raises Dry::Data::ConstraintError
-float[3.2] # => 3.2
-float["3.2"] # "3.2"
-
-# strict type-check category
-int = Dry::Data["strict.int"]
-
-int[1] # => 1
-int['1'] # => raises TypeError
-
# coercible type-check group
-string = Dry::Data["coercible.string"]
-array = Dry::Data["coercible.array"]
+Types::Coercible::String[:foo] # => 'foo'
+Types::Coercible::Array[:foo] # => [:foo]
-string[:foo] # => 'foo'
-array[:foo] # => [:foo]
-
# form group
-date = Dry::Data["form.date"]
-date['2015-11-29'] # => #<Date: 2015-11-29 ((2457356j,0s,0n),+0s,2299161j)>
+Types::Form::Date['2015-11-29'] # => #<Date: 2015-11-29 ((2457356j,0s,0n),+0s,2299161j)>
```
-### Optional types
+### Optional Types
All built-in types have their optional versions too, you can access them under
-`"maybe.strict"` and `"maybe.coercible"` categories:
+`"Types::Maybe::Strict"` and `"Maybe::Coercible"` categories:
``` ruby
-maybe_int = Dry::Data["maybe.strict.int"]
+Types::Maybe::Int[nil] # None
+Types::Maybe::Int[123] # Some(123)
-maybe_int[nil] # None
-maybe_int[123] # Some(123)
-
-maybe_coercible_float = Dry::Data["maybe.coercible.float"]
-
-maybe_int[nil] # None
-maybe_int['12.3'] # Some(12.3)
+Types::Maybe::Coercible::Float[nil] # None
+Types::Maybe::Coercible::Float['12.3'] # Some(12.3)
```
You can define your own optional types too:
``` ruby
-maybe_string = Dry::Data["string"].optional
+maybe_string = Types::Strict::String.optional
maybe_string[nil]
# => None
maybe_string[nil].fmap(&:upcase)
@@ -162,23 +237,35 @@
maybe_string['something'].fmap(&:upcase).value
# => "SOMETHING"
```
+### Defaults
+
+A type with a default value will return the configured value when the input is `nil`:
+
+``` ruby
+PostStatus = Types::Strict::String.default('draft')
+
+PostStatus[nil] # "draft"
+PostStatus["published"] # "published"
+PostStatus[true] # raises ConstraintError
+```
+
### Sum-types
You can specify sum types using `|` operator, it is an explicit way of defining
what are the valid types of a value.
In example `dry-data` defines `bool` type which is a sum-type consisting of `true`
-and `false` types which is expressed as `Dry::Data['true'] | Dry::Data['false']`
+and `false` types which is expressed as `Types::True | Types::False`
(and it has its strict version, too).
Another common case is defining that something can be either `nil` or something else:
``` ruby
-nil_or_string = Dry::Data['strict.nil'] | Dry::Data['strict.string']
+nil_or_string = Types::Nil | Types::Strict::String
nil_or_string[nil] # => nil
nil_or_string["hello"] # => "hello"
```
@@ -189,93 +276,41 @@
a lower level guarantee that you're not instantiating objects that are broken.
All types support constraints API, but not all constraints are suitable for a
particular primitive, it's up to you to set up constraints that make sense.
-Under the hood it uses [`dry-validation`](https://github.com/dryrb/dry-validation)
+Under the hood it uses [`dry-logic`](https://github.com/dryrb/dry-logic)
and all of its predicates are supported.
-IMPORTANT: `dry-data` does not have a runtime dependency on `dry-validation` so
-if you want to use contrained types you need to add it to your Gemfile
-
-If you want to use constrained type you need to require it explicitly:
-
``` ruby
-require "dry/data/type/constrained"
-```
+string = Types::Strict::String.constrained(min_size: 3)
-``` ruby
-string = Dry::Data["strict.string"].constrained(min_size: 3)
-
string['foo']
# => "foo"
string['fo']
# => Dry::Data::ConstraintError: "fo" violates constraints
-email = Dry::Data['strict.string'].constrained(
+email = Types::Strict::String.constrained(
format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i
)
email["jane@doe.org"]
# => "jane@doe.org"
email["jane"]
# => Dry::Data::ConstraintError: "fo" violates constraints
```
-### Setting Type Constants
+### Enums
-Types can be stored as easily accessible constants in a configured namespace:
-
-``` ruby
-module Types; end
-
-Dry::Data.configure do |config|
- config.namespace = Types
-end
-
-# after defining your custom types (if you've got any) you can finalize setup
-Dry::Data.finalize
-
-# this defines all types under your namespace
-Types::Coercible::String
-# => #<Dry::Data::Type:0x007feffb104aa8 @constructor=#<Method: Kernel.String>, @primitive=String>
-```
-
-With types accessible as constants you can easily compose more complex types,
-like sum-types or constrained types, in hash schemas or structs:
-
-``` ruby
-Dry::Data.configure do |config|
- config.namespace = Types
-end
-
-Dry::Data.finalize
-
-module Types
- Email = String.constrained(format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i)
- Age = Int.constrained(gt: 18)
-end
-
-class User < Dry::Data::Struct
- attribute :name, Types::String
- attribute :email, Types::Email
- attribute :age, Types::Age
-end
-```
-
-### Defining Enums
-
In many cases you may want to define an enum. For example in a blog application
a post may have a finite list of statuses. Apart from accessing the current status
value it is useful to have all possible values accessible too. Furthermore an
enum is a `int => value` map, so you can store integers somewhere and have them
mapped to enum values conveniently.
-You can define enums for every type but it probably only makes sense for `string`:
-
``` ruby
# assuming we have types loaded into `Types` namespace
# we can easily define an enum for our post struct
class Post < Dry::Data::Struct
Statuses = Types::Strict::String.enum('draft', 'published', 'archived')
@@ -301,80 +336,105 @@
# nil is considered as something silly too
Post::Statuses[nil]
# => Dry::Data::ConstraintError: nil violates constraints
```
-### Defining a hash with explicit schema
+### Hashes
The built-in hash type has constructors that you can use to define hashes with
explicit schemas and coercible values using the built-in types.
-### Hash Schema
+#### Hash Schema
``` ruby
# using simple kernel coercions
-hash = Dry::Data['hash'].schema(name: 'string', age: 'coercible.int')
+hash = Types::Hash.schema(name: Types::String, age: Types::Coercible::Int)
hash[name: 'Jane', age: '21']
# => { :name => "Jane", :age => 21 }
# using form param coercions
-hash = Dry::Data['hash'].schema(name: 'string', birthdate: 'form.date')
+hash = Types::Hash.schema(name: Types::String, birthdate: Form::Date)
hash[name: 'Jane', birthdate: '1994-11-11']
# => { :name => "Jane", :birthdate => #<Date: 1994-11-11 ((2449668j,0s,0n),+0s,2299161j)> }
```
-### Strict Hash
+#### Strict Schema
Strict hash will raise errors when keys are missing or value types are incorrect.
``` ruby
-hash = Dry::Data['hash'].strict(name: 'string', age: 'coercible.int')
+hash = Types::Hash.strict(name: 'string', age: 'coercible.int')
hash[email: 'jane@doe.org', name: 'Jane', age: 21]
# => Dry::Data::SchemaKeyError: :email is missing in Hash input
```
-### Symbolized Hash
+#### Symbolized Schema
Symbolized hash will turn string key names into symbols
``` ruby
-hash = Dry::Data['hash'].symbolized(name: 'string', age: 'coercible.int')
+hash = Types::Hash.symbolized(name: Types::String, age: Types::Coercible::Int)
hash['name' => 'Jane', 'age' => '21']
# => { :name => "Jane", :age => 21 }
```
-### Defining a struct
+### Arrays
-You can define struct objects which will have attribute readers for specified
-attributes using a simple dsl:
+The built-in array type supports defining member type:
``` ruby
+PostStatuses = Types::Strict::Array.member(Types::Coercible::String)
+
+PostStatuses[[:foo, :bar]] # ["foo", "bar"]
+```
+
+### Structs
+
+You can define struct objects which will have readers for specified attributes
+using a simple dsl:
+
+``` ruby
class User < Dry::Data::Struct
- attribute :name, "maybe.coercible.string"
- attribute :age, "coercible.int"
+ attribute :name, Types::Maybe::Coercible::String
+ attribute :age, Types::Coercible::Int
end
-# becomes available like any other type
-user_type = Dry::Data["user"]
+user = User.new(name: nil, age: '21')
-user = user_type[name: nil, age: '21']
-
user.name # None
user.age # 21
-user = user_type[name: 'Jane', age: '21']
+user = User(name: 'Jane', age: '21')
user.name # => Some("Jane")
user.age # => 21
```
+### Values
+
+You can define value objects which will behave like structs and have equality
+methods too:
+
+``` ruby
+class Location < Dry::Data::Value
+ attribute :lat, Types::Strict::Float
+ attribute :lat, Types::Strict::Float
+end
+
+loc1 = Location.new(lat: 1.23, lng: 4.56)
+loc2 = Location.new(lat: 1.23, lng: 4.56)
+
+loc1 == loc2
+# true
+```
+
## Status and Roadmap
-This library is in an early stage of development but you are encauraged to try it
+This library is in an early stage of development but you are encouraged to try it
out and provide feedback.
For planned features check out [the issues](https://github.com/dryrb/dry-data/labels/feature).
## Development