dry-data 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +29 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +11 -0
- data/LICENSE +20 -0
- data/README.md +172 -0
- data/Rakefile +6 -0
- data/benchmarks/basic.rb +57 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/dry-data.gemspec +37 -0
- data/lib/dry/data.rb +67 -0
- data/lib/dry/data/container.rb +7 -0
- data/lib/dry/data/dsl.rb +15 -0
- data/lib/dry/data/struct.rb +45 -0
- data/lib/dry/data/sum_type.rb +49 -0
- data/lib/dry/data/type.rb +58 -0
- data/lib/dry/data/type/array.rb +18 -0
- data/lib/dry/data/type/hash.rb +33 -0
- data/lib/dry/data/types.rb +61 -0
- data/lib/dry/data/version.rb +5 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 706c2f7f4ad11b612b658585553e3bb850b53b76
|
4
|
+
data.tar.gz: a93749b4327258e7e1a76893b4e4e9f6526b9611
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 37ceb712ef8b4c3e62791694460d0f1a010ff0e33ea8c9db971bf99455fdf1434501f5c2003be3b8b1856ee5a138f2d78a717ff07117278b0722f5de9e055f8e
|
7
|
+
data.tar.gz: 752bdf83d32e5909bd183d582486b7132d19e58c5d2a50898286246c20a045476cdbfc63cddc5e324dc716f6c0a13677b3e5a352daaa50159c49ae9d10d7e9dd
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: false
|
3
|
+
cache: bundler
|
4
|
+
bundler_args: --without console
|
5
|
+
script:
|
6
|
+
- bundle exec rake spec
|
7
|
+
rvm:
|
8
|
+
- 2.0
|
9
|
+
- 2.1
|
10
|
+
- 2.2
|
11
|
+
- rbx-2
|
12
|
+
- jruby
|
13
|
+
- ruby-head
|
14
|
+
- jruby-head
|
15
|
+
env:
|
16
|
+
global:
|
17
|
+
- JRUBY_OPTS='--dev -J-Xmx1024M'
|
18
|
+
matrix:
|
19
|
+
allow_failures:
|
20
|
+
- rvm: ruby-head
|
21
|
+
- rvm: jruby-head
|
22
|
+
notifications:
|
23
|
+
email: false
|
24
|
+
webhooks:
|
25
|
+
urls:
|
26
|
+
- https://webhooks.gitter.im/e/19098b4253a72c9796db
|
27
|
+
on_success: change # options: [always|never|change] default: always
|
28
|
+
on_failure: always # options: [always|never|change] default: always
|
29
|
+
on_start: false # default: false
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013-2014 Piotr Solnica
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
# Dry::Data <a href="https://gitter.im/dryrb/chat" target="_blank"></a>
|
2
|
+
|
3
|
+
<a href="https://rubygems.org/gems/dry-data" target="_blank"></a>
|
4
|
+
<a href="https://travis-ci.org/dryrb/dry-data" target="_blank"></a>
|
5
|
+
<a href="https://gemnasium.com/dryrb/dry-data" target="_blank"></a>
|
6
|
+
<a href="https://codeclimate.com/github/dryrb/dry-data" target="_blank"></a>
|
7
|
+
<a href="http://inch-ci.org/github/dryrb/dry-data" target="_blank"></a>
|
8
|
+
|
9
|
+
A simple type-system for Ruby respecting ruby's built-in coercion mechanisms.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'dry-data'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install dry-data
|
26
|
+
|
27
|
+
## Why?
|
28
|
+
|
29
|
+
Unlike seemingly similar libraries like virtus, attrio, fast_attrs, attribs etc.
|
30
|
+
`Dry::Data` provides you an interface to explicitly specify data types you want
|
31
|
+
to use in your application domain which gives you type-safety and *simple* coercion
|
32
|
+
mechanism using built-in coercion methods on the kernel.
|
33
|
+
|
34
|
+
Main difference is that `Dry::Data` is not designed to handle all kinds of complex
|
35
|
+
coercions that are typically required when dealing with, let's say, form params
|
36
|
+
in a web application. Its primary focus is to allow you to specify the exact shape
|
37
|
+
of the custom application data types to avoid silly bugs that are often hard to debug
|
38
|
+
(`NoMethodError: undefined method `size' for nil:NilClass` anyone?).
|
39
|
+
|
40
|
+
## Usage
|
41
|
+
|
42
|
+
Primary usage of this library is defining domain data types that your application
|
43
|
+
will work with. The interface consists of lower-level type definitions and a higher-level
|
44
|
+
virtus-like interface for defining structs.
|
45
|
+
|
46
|
+
|
47
|
+
### Accessing built-in types
|
48
|
+
|
49
|
+
Coercible types using kernel coercion methods:
|
50
|
+
|
51
|
+
- `string`
|
52
|
+
- `int`
|
53
|
+
- `float`
|
54
|
+
- `decimal`
|
55
|
+
- `array`
|
56
|
+
- `hash`
|
57
|
+
|
58
|
+
Non-coercible:
|
59
|
+
|
60
|
+
- `nil`
|
61
|
+
- `true`
|
62
|
+
- `false`
|
63
|
+
- `date`
|
64
|
+
- `date_time`
|
65
|
+
- `time`
|
66
|
+
|
67
|
+
More types will be added soon.
|
68
|
+
|
69
|
+
Types are grouped under 4 categories:
|
70
|
+
|
71
|
+
- default: pass-through without any checks
|
72
|
+
- `strict` - doesn't coerce and checks the input type against the primitive class
|
73
|
+
- `coercible` - tries to coerce and raises type-error if it failed
|
74
|
+
- `maybe` - accepts either a nil or something else
|
75
|
+
|
76
|
+
``` ruby
|
77
|
+
# default passthrough category
|
78
|
+
float = Dry::Data["float"]
|
79
|
+
|
80
|
+
float[3.2] # => 3.2
|
81
|
+
float["3.2"] # "3.2"
|
82
|
+
|
83
|
+
# strict type-check category
|
84
|
+
int = Dry::Data["strict.int"]
|
85
|
+
|
86
|
+
int[1] # => 1
|
87
|
+
int['1'] # => raises TypeError
|
88
|
+
|
89
|
+
# coercible type-check group
|
90
|
+
string = Dry::Data["coercible.string"]
|
91
|
+
array = Dry::Data["coercible.array"]
|
92
|
+
|
93
|
+
string[:foo] # => 'foo'
|
94
|
+
array[:foo] # => [:foo]
|
95
|
+
```
|
96
|
+
|
97
|
+
### Optional types
|
98
|
+
|
99
|
+
All built-in types have their optional versions too, you can access them under
|
100
|
+
`"maybe.strict"` and `"maybe.coercible"` categories:
|
101
|
+
|
102
|
+
``` ruby
|
103
|
+
maybe_int = Dry::Data["maybe.strict.int"]
|
104
|
+
|
105
|
+
maybe_int[nil] # None
|
106
|
+
maybe_int[123] # Some(123)
|
107
|
+
|
108
|
+
maybe_coercible_float = Dry::Data["maybe.coercible.float"]
|
109
|
+
|
110
|
+
maybe_int[nil] # None
|
111
|
+
maybe_int['12.3'] # Some(12.3)
|
112
|
+
```
|
113
|
+
|
114
|
+
You can define your own optional types too:
|
115
|
+
|
116
|
+
``` ruby
|
117
|
+
maybe_string = Dry::Data["nil"] | Dry::Data["string"]
|
118
|
+
|
119
|
+
maybe_string[nil]
|
120
|
+
# => None
|
121
|
+
|
122
|
+
maybe_string[nil].fmap(&:upcase)
|
123
|
+
# => None
|
124
|
+
|
125
|
+
maybe_string['something']
|
126
|
+
# => Some('something')
|
127
|
+
|
128
|
+
maybe_string['something'].fmap(&:upcase)
|
129
|
+
# => Some('SOMETHING')
|
130
|
+
|
131
|
+
maybe_string['something'].fmap(&:upcase).value
|
132
|
+
# => "SOMETHING"
|
133
|
+
```
|
134
|
+
|
135
|
+
### Defining a struct
|
136
|
+
|
137
|
+
``` ruby
|
138
|
+
class User < Dry::Data::Struct
|
139
|
+
attribute :name, "maybe.coercible.string"
|
140
|
+
attribute :age, "coercible.int"
|
141
|
+
end
|
142
|
+
|
143
|
+
# becomes available like any other type
|
144
|
+
user_type = Dry::Data["user"]
|
145
|
+
|
146
|
+
user = user_type[name: nil, age: '21']
|
147
|
+
|
148
|
+
user.name # None
|
149
|
+
user.age # 21
|
150
|
+
|
151
|
+
user = user_type[name: 'Jane', age: '21']
|
152
|
+
|
153
|
+
user.name # => Some("Jane")
|
154
|
+
user.age # => 21
|
155
|
+
```
|
156
|
+
|
157
|
+
## WIP
|
158
|
+
|
159
|
+
This is early alpha with a rough plan to:
|
160
|
+
|
161
|
+
* Add constrained types (ie a string with a strict length, a number with a strict range etc.)
|
162
|
+
* Benchmark against other libs and make sure it's fast enough
|
163
|
+
|
164
|
+
## Development
|
165
|
+
|
166
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
167
|
+
|
168
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
169
|
+
|
170
|
+
## Contributing
|
171
|
+
|
172
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/dryrb/dry-data.
|
data/Rakefile
ADDED
data/benchmarks/basic.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'dry/data'
|
2
|
+
require 'virtus'
|
3
|
+
require 'fast_attributes'
|
4
|
+
require 'attrio'
|
5
|
+
require 'ostruct'
|
6
|
+
|
7
|
+
require 'benchmark/ips'
|
8
|
+
|
9
|
+
class VirtusUser
|
10
|
+
include Virtus.model
|
11
|
+
|
12
|
+
attribute :name, String
|
13
|
+
attribute :age, Integer
|
14
|
+
end
|
15
|
+
|
16
|
+
class FastUser
|
17
|
+
extend FastAttributes
|
18
|
+
|
19
|
+
define_attributes initialize: true, attributes: true do
|
20
|
+
attribute :name, String
|
21
|
+
attribute :age, Integer
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class AttrioUser
|
26
|
+
include Attrio
|
27
|
+
|
28
|
+
define_attributes do
|
29
|
+
attr :name, String
|
30
|
+
attr :age, Integer
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(attributes = {})
|
34
|
+
self.attributes = attributes
|
35
|
+
end
|
36
|
+
|
37
|
+
def attributes=(attributes = {})
|
38
|
+
attributes.each do |attr,value|
|
39
|
+
self.send("#{attr}=", value) if self.respond_to?("#{attr}=")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class DryDataUser < Dry::Data::Struct
|
45
|
+
attributes(name: 'coercible.string', age: 'coercible.int')
|
46
|
+
end
|
47
|
+
|
48
|
+
puts DryDataUser.new(name: 'Jane', age: '21').inspect
|
49
|
+
|
50
|
+
Benchmark.ips do |x|
|
51
|
+
x.report('virtus') { VirtusUser.new(name: 'Jane', age: '21') }
|
52
|
+
x.report('fast_attributes') { FastUser.new(name: 'Jane', age: '21') }
|
53
|
+
x.report('attrio') { AttrioUser.new(name: 'Jane', age: '21') }
|
54
|
+
x.report('dry-data') { DryDataUser.new(name: 'Jane', age: '21') }
|
55
|
+
|
56
|
+
x.compare!
|
57
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "dry/data"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/dry-data.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dry/data/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dry-data"
|
8
|
+
spec.version = Dry::Data::VERSION.dup
|
9
|
+
spec.authors = ["Piotr Solnica"]
|
10
|
+
spec.email = ["piotr.solnica@gmail.com"]
|
11
|
+
spec.license = 'MIT'
|
12
|
+
|
13
|
+
spec.summary = 'Simple type-system for Ruby'
|
14
|
+
spec.description = spec.summary
|
15
|
+
spec.homepage = "https://github.com/dryrb/dry-data"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_runtime_dependency 'dry-container', '~> 0.2'
|
31
|
+
spec.add_runtime_dependency 'inflecto', '~> 0.0.0', '>= 0.0.2'
|
32
|
+
spec.add_runtime_dependency 'kleisli', '~> 0.2'
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
35
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.3"
|
37
|
+
end
|
data/lib/dry/data.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'date'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'dry-container'
|
6
|
+
require 'inflecto'
|
7
|
+
|
8
|
+
require 'dry/data/version'
|
9
|
+
require 'dry/data/container'
|
10
|
+
require 'dry/data/type'
|
11
|
+
require 'dry/data/struct'
|
12
|
+
require 'dry/data/dsl'
|
13
|
+
|
14
|
+
module Dry
|
15
|
+
module Data
|
16
|
+
class SchemaError < TypeError
|
17
|
+
def initialize(key, value)
|
18
|
+
super("#{value.inspect} (#{value.class}) has invalid type for :#{key}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class SchemaKeyError < KeyError
|
23
|
+
def initialize(key)
|
24
|
+
super(":#{key} is missing in Hash input")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
StructError = Class.new(TypeError)
|
29
|
+
|
30
|
+
TYPE_SPEC_REGEX = %r[(.+)<(.+)>].freeze
|
31
|
+
|
32
|
+
def self.container
|
33
|
+
@container ||= Container.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.register(name, type)
|
37
|
+
container.register(name, type)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.[](name)
|
41
|
+
type_map.fetch(name) do
|
42
|
+
result = name.match(TYPE_SPEC_REGEX)
|
43
|
+
|
44
|
+
type =
|
45
|
+
if result
|
46
|
+
type_id, member_id = result[1..2]
|
47
|
+
container[type_id].member(self[member_id])
|
48
|
+
else
|
49
|
+
container[name]
|
50
|
+
end
|
51
|
+
|
52
|
+
type_map[name] = type
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.type(*args, &block)
|
57
|
+
dsl = DSL.new(container)
|
58
|
+
block ? yield(dsl) : registry[args.first]
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.type_map
|
62
|
+
@type_map ||= {}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
require 'dry/data/types' # load built-in types
|
data/lib/dry/data/dsl.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
class Struct
|
4
|
+
class << self
|
5
|
+
attr_reader :constructor
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.inherited(klass)
|
9
|
+
super
|
10
|
+
name = Inflecto.underscore(klass).gsub('/', '.')
|
11
|
+
Data.register(name, Type.new(klass.method(:new), klass))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.attribute(*args)
|
15
|
+
attributes(Hash[[args]])
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.attributes(new_schema)
|
19
|
+
prev_schema = schema || {}
|
20
|
+
|
21
|
+
@schema = prev_schema.merge(new_schema)
|
22
|
+
@constructor = Data['coercible.hash'].schema(schema)
|
23
|
+
|
24
|
+
attr_reader(*(new_schema.keys - prev_schema.keys))
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.schema
|
30
|
+
super_schema = superclass.respond_to?(:schema) ? superclass.schema : {}
|
31
|
+
super_schema.merge(@schema || {})
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.new(attributes)
|
35
|
+
super(constructor[attributes])
|
36
|
+
rescue SchemaError, SchemaKeyError => e
|
37
|
+
raise StructError, "[#{self}.new] #{e.message}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(attributes)
|
41
|
+
attributes.each { |key, value| instance_variable_set("@#{key}", value) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'kleisli'
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Data
|
5
|
+
def self.SumType(left, right)
|
6
|
+
klass =
|
7
|
+
if left.primitive == NilClass
|
8
|
+
SumType::Optional
|
9
|
+
else
|
10
|
+
SumType
|
11
|
+
end
|
12
|
+
klass.new(left, right)
|
13
|
+
end
|
14
|
+
|
15
|
+
class SumType
|
16
|
+
attr_reader :left
|
17
|
+
|
18
|
+
attr_reader :right
|
19
|
+
|
20
|
+
class Optional < SumType
|
21
|
+
def call(input)
|
22
|
+
Maybe(super(input))
|
23
|
+
end
|
24
|
+
alias_method :[], :call
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(left, right)
|
28
|
+
@left, @right = left, right
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
[left, right].map(&:name).join(' | ')
|
33
|
+
end
|
34
|
+
|
35
|
+
def call(input)
|
36
|
+
begin
|
37
|
+
left[input]
|
38
|
+
rescue TypeError
|
39
|
+
right[input]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
alias_method :[], :call
|
43
|
+
|
44
|
+
def valid?(input)
|
45
|
+
left.valid?(input) || right.valid?(input)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'dry/data/type/hash'
|
2
|
+
require 'dry/data/type/array'
|
3
|
+
|
4
|
+
require 'dry/data/sum_type'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Data
|
8
|
+
class Type
|
9
|
+
attr_reader :constructor
|
10
|
+
|
11
|
+
attr_reader :primitive
|
12
|
+
|
13
|
+
def self.[](primitive)
|
14
|
+
if primitive == ::Array
|
15
|
+
Type::Array
|
16
|
+
elsif primitive == ::Hash
|
17
|
+
Type::Hash
|
18
|
+
else
|
19
|
+
Type
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.strict_constructor(primitive, input)
|
24
|
+
if input.is_a?(primitive)
|
25
|
+
input
|
26
|
+
else
|
27
|
+
raise TypeError, "#{input.inspect} has invalid type"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.passthrough_constructor(input)
|
32
|
+
input
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(constructor, primitive)
|
36
|
+
@constructor = constructor
|
37
|
+
@primitive = primitive
|
38
|
+
end
|
39
|
+
|
40
|
+
def name
|
41
|
+
primitive.name
|
42
|
+
end
|
43
|
+
|
44
|
+
def call(input)
|
45
|
+
constructor[input]
|
46
|
+
end
|
47
|
+
alias_method :[], :call
|
48
|
+
|
49
|
+
def valid?(input)
|
50
|
+
input.instance_of?(primitive)
|
51
|
+
end
|
52
|
+
|
53
|
+
def |(other)
|
54
|
+
Data.SumType(self, other)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
class Type
|
4
|
+
class Array < Type
|
5
|
+
def self.constructor(array_constructor, value_constructor, input)
|
6
|
+
array_constructor[input].map(&value_constructor)
|
7
|
+
end
|
8
|
+
|
9
|
+
def member(type)
|
10
|
+
self.class.new(
|
11
|
+
self.class.method(:constructor).to_proc.curry.(constructor, type.constructor),
|
12
|
+
primitive
|
13
|
+
)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
class Type
|
4
|
+
class Hash < Type
|
5
|
+
def self.constructor(hash_constructor, value_constructors, input)
|
6
|
+
attributes = hash_constructor[input]
|
7
|
+
|
8
|
+
value_constructors.each_with_object({}) do |(key, value_constructor), result|
|
9
|
+
begin
|
10
|
+
value = attributes.fetch(key)
|
11
|
+
result[key] = value_constructor[value]
|
12
|
+
rescue TypeError
|
13
|
+
raise SchemaError.new(key, value)
|
14
|
+
rescue KeyError
|
15
|
+
raise SchemaKeyError.new(key)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def schema(type_map)
|
21
|
+
value_constructors = type_map.each_with_object({}) { |(name, type_id), result|
|
22
|
+
result[name] = Data[type_id]
|
23
|
+
}
|
24
|
+
|
25
|
+
self.class.new(
|
26
|
+
self.class.method(:constructor).to_proc.curry.(constructor, value_constructors),
|
27
|
+
primitive
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Dry
|
2
|
+
module Data
|
3
|
+
COERCIBLE = {
|
4
|
+
string: String,
|
5
|
+
int: Integer,
|
6
|
+
float: Float,
|
7
|
+
decimal: BigDecimal,
|
8
|
+
array: Array,
|
9
|
+
hash: Hash
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
NON_COERCIBLE = {
|
13
|
+
nil: NilClass,
|
14
|
+
true: TrueClass,
|
15
|
+
false: FalseClass,
|
16
|
+
date: Date,
|
17
|
+
date_time: DateTime,
|
18
|
+
time: Time
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
ALL_PRIMITIVES = COERCIBLE.merge(NON_COERCIBLE).freeze
|
22
|
+
|
23
|
+
# Register built-in primitive types with kernel coercion methods
|
24
|
+
COERCIBLE.each do |name, primitive|
|
25
|
+
register(
|
26
|
+
"coercible.#{name}",
|
27
|
+
Type[primitive].new(Kernel.method(primitive.name), primitive)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Register built-in types that are non-coercible through kernel methods
|
32
|
+
ALL_PRIMITIVES.each do |name, primitive|
|
33
|
+
register(
|
34
|
+
"strict.#{name}",
|
35
|
+
Type[primitive].new(Type.method(:strict_constructor).to_proc.curry.(primitive), primitive)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Register built-in types that are non-coercible through kernel methods
|
40
|
+
ALL_PRIMITIVES.each do |name, primitive|
|
41
|
+
register(
|
42
|
+
name.to_s,
|
43
|
+
Type[primitive].new(Type.method(:passthrough_constructor), primitive)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Register non-coercible maybe types
|
48
|
+
ALL_PRIMITIVES.each do |name, primitive|
|
49
|
+
next if name == :nil
|
50
|
+
register("maybe.strict.#{name}", self["strict.nil"] | self["strict.#{name}"])
|
51
|
+
end
|
52
|
+
|
53
|
+
# Register coercible maybe types
|
54
|
+
COERCIBLE.each do |name, primitive|
|
55
|
+
register("maybe.coercible.#{name}", self["strict.nil"] | self["coercible.#{name}"])
|
56
|
+
end
|
57
|
+
|
58
|
+
# Register :bool since it's common and not a built-in Ruby type :(
|
59
|
+
register("strict.bool", self["strict.true"] | self["strict.false"])
|
60
|
+
end
|
61
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dry-data
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Piotr Solnica
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-10-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-container
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: inflecto
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.0.0
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 0.0.2
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.0.0
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.0.2
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: kleisli
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0.2'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0.2'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: bundler
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.7'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.7'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rake
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '10.0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '10.0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rspec
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.3'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.3'
|
103
|
+
description: Simple type-system for Ruby
|
104
|
+
email:
|
105
|
+
- piotr.solnica@gmail.com
|
106
|
+
executables: []
|
107
|
+
extensions: []
|
108
|
+
extra_rdoc_files: []
|
109
|
+
files:
|
110
|
+
- ".gitignore"
|
111
|
+
- ".rspec"
|
112
|
+
- ".travis.yml"
|
113
|
+
- CHANGELOG.md
|
114
|
+
- Gemfile
|
115
|
+
- LICENSE
|
116
|
+
- README.md
|
117
|
+
- Rakefile
|
118
|
+
- benchmarks/basic.rb
|
119
|
+
- bin/console
|
120
|
+
- bin/setup
|
121
|
+
- dry-data.gemspec
|
122
|
+
- lib/dry/data.rb
|
123
|
+
- lib/dry/data/container.rb
|
124
|
+
- lib/dry/data/dsl.rb
|
125
|
+
- lib/dry/data/struct.rb
|
126
|
+
- lib/dry/data/sum_type.rb
|
127
|
+
- lib/dry/data/type.rb
|
128
|
+
- lib/dry/data/type/array.rb
|
129
|
+
- lib/dry/data/type/hash.rb
|
130
|
+
- lib/dry/data/types.rb
|
131
|
+
- lib/dry/data/version.rb
|
132
|
+
homepage: https://github.com/dryrb/dry-data
|
133
|
+
licenses:
|
134
|
+
- MIT
|
135
|
+
metadata:
|
136
|
+
allowed_push_host: https://rubygems.org
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
require_paths:
|
140
|
+
- lib
|
141
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: '0'
|
151
|
+
requirements: []
|
152
|
+
rubyforge_project:
|
153
|
+
rubygems_version: 2.4.5
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: Simple type-system for Ruby
|
157
|
+
test_files: []
|