rschema_hamster 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Readme.md +117 -31
- data/lib/rschema_hamster.rb +4 -93
- data/lib/rschema_hamster/dsl.rb +12 -8
- data/lib/rschema_hamster/extend_rschema_dsl.rb +13 -0
- data/lib/rschema_hamster/hamster_ext.rb +89 -0
- data/lib/rschema_hamster/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 02922c410a8a09d62e7ab3b3484d262dbd1aa020
|
4
|
+
data.tar.gz: 1ff95e47d3a34bb1442770a815837615ff3fa32e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: df924a013064ef6c5e6035959d51facb4e558215aef5b01d8fb796524a06308b9313bc1507902d41772d8ef49368fa077a3983574f12025a5c1a4c130b945171
|
7
|
+
data.tar.gz: 98663647b0b6e0947ad26de724ad2ba504532bfed980772946ad8e618a9e877f2b5efb1d79b0fdc048f3483f2025e8a64aef43cdbe7e124dcdb6e60ce773eead
|
data/Readme.md
CHANGED
@@ -1,51 +1,137 @@
|
|
1
1
|
# RSchema-Hamster
|
2
2
|
|
3
|
-
|
3
|
+
Use schemas to define the shape of your immutable data structures. Combines the declarative data-driven schemas from [RSchema][RSCHEMA-DOC] with the persistent collections from [Hamster][HAMSTER-DOC].
|
4
4
|
|
5
|
-
|
6
|
-
Email = String
|
7
|
-
PostalCode = String
|
5
|
+
### Baseline
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
7
|
+
Peek at the examples in this Readme, but head over to RSchema's docs for a much better intro to using Ruby data to define and validate data structures... RSchema-Hamster merely extends RSchema by bringing Hamster's classes into the fold, and you can still mix-and-match Ruby's built-in collections as well as RSchema's DSL for generic and optional values. Likewise, see Hamster's docs for an intro to immutable structures in Ruby.
|
8
|
+
|
9
|
+
[![Build Status](https://travis-ci.org/dcrosby42/rschema_hamster.svg?branch=master)](https://travis-ci.org/dcrosby42/rschema_hamster)
|
10
|
+
|
11
|
+
## What this gem adds
|
12
|
+
|
13
|
+
### Hamster::Hash as a schema
|
14
|
+
Hamster's immutable Hash structures may be used as schemas, and will be validated using the same algorithm RSchema uses to evaluate Ruby's built-in Hash, with the additional requirement that values are, in fact, Hamster::Hashes.
|
13
15
|
|
16
|
+
*Example*:
|
17
|
+
```ruby
|
14
18
|
Address = Hamster.hash(
|
15
19
|
street: String,
|
16
|
-
postal_code:
|
20
|
+
postal_code: Integer
|
17
21
|
)
|
18
22
|
|
19
|
-
|
20
|
-
|
21
|
-
addresses: Hamster.vector(Address)
|
22
|
-
)
|
23
|
+
RSchema.validate!(Address, Hamster.hash(street: "187 Drury Ln", postal_code: 1234))
|
24
|
+
```
|
23
25
|
|
24
|
-
|
26
|
+
### Generic Hashes: consistent key/value types
|
27
|
+
Use RSchemaHamster::DSL.hamster_hash_of(s1 => s2) to describe a variable-sized Hash whose keys all conform to s1 and whose values conform to s2.
|
28
|
+
|
29
|
+
*Example*:
|
30
|
+
```ruby
|
31
|
+
PlayerScores = RSchemaHanster.schema {
|
32
|
+
hamster_hash_of(String => Integer)
|
33
|
+
}
|
34
|
+
|
35
|
+
RSchema.validate!(PlayerScores, Hamster.hash("Link" => 12, "Samus" => 132))
|
25
36
|
```
|
26
37
|
|
27
|
-
|
38
|
+
### Hamster::Vector as a schema
|
39
|
+
Hamster's immutable Vector structures may be used as schemas, and will be validated using the same rules and alternatives RSchema uses to evaluate Ruby's built-in Array, with the additional requirement that values are, in fact, Hamster::Vectors.
|
40
|
+
|
41
|
+
Vectors with length > 1 are interprested as tuples, validating Vectors of same length whose nth element validates according to the nth subschema.
|
28
42
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
** maybe
|
35
|
-
** hamster_hash_of
|
36
|
-
** hamster_set_of - Hasmter::Set -(GenericHamsterSetSchema)
|
37
|
-
** enum
|
43
|
+
```ruby
|
44
|
+
Token = Hamster.vector(Symbol, String)
|
45
|
+
|
46
|
+
RSchema.validate!(Token, Hamster.vector(:keyword, "require"))
|
47
|
+
```
|
38
48
|
|
39
|
-
|
49
|
+
Single-length Vectors validates a 0-or-more-length Vector whose elements validate according to the single subschema.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
Tokens = Hamster.vector(Token)
|
53
|
+
|
54
|
+
RSchema.validate!(Tokens, Hamster.vector(
|
55
|
+
Hamster.vector(:keyword, "require"),
|
56
|
+
Hamster.vector(:quot, "'"),
|
57
|
+
Hamster.vector(:string_lit, "rschema_hamster"),
|
58
|
+
Hamster.vector(:quot, "'")
|
59
|
+
))
|
60
|
+
```
|
40
61
|
|
41
|
-
Project:
|
42
|
-
* Proper readme
|
43
|
-
* .gem
|
44
62
|
|
45
|
-
|
63
|
+
### Hamster::List as a schema
|
64
|
+
(Actually Hamster::Cons) currently implemented in parallel to Hamster::Vector. Consider making this a "lazy validated" schema?
|
46
65
|
|
47
|
-
|
48
|
-
|
66
|
+
### Sets
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
Words = RSchemaHamster.schema {
|
70
|
+
hamster_set_of(String)
|
71
|
+
}
|
72
|
+
RSchema.validate!(TheSyms, Hamster.set("some", "of", "words"))
|
73
|
+
```
|
74
|
+
|
75
|
+
## Example:
|
76
|
+
|
77
|
+
The schema:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
module OrderReport::Schema
|
81
|
+
Name = String
|
82
|
+
Id = Integer
|
83
|
+
Dollars = BigDecimal
|
84
|
+
|
85
|
+
Totals = Hamster.hash(
|
86
|
+
gross: Dollars,
|
87
|
+
tax: Dollars,
|
88
|
+
fee: Dollars,
|
89
|
+
net: Dollars,
|
90
|
+
)
|
91
|
+
|
92
|
+
OrderRow = Hamster.hash(
|
93
|
+
order_id: Id,
|
94
|
+
order_number: Name,
|
95
|
+
order_totals: Totals,
|
96
|
+
)
|
97
|
+
|
98
|
+
MarketBlock = Hamster.hash(
|
99
|
+
market_id: Id,
|
100
|
+
market_name: Name,
|
101
|
+
account_dropdown: RSchemaHamster.schema {
|
102
|
+
hamster_hash_of(Name => Id)
|
103
|
+
},
|
104
|
+
order_rows: Hamster.vector(OrderRow),
|
105
|
+
market_totals: Totals
|
106
|
+
)
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
The data (Hamster.from is a convenience that recursively converts Hash and Array to Hamster::Hash and Hamster::Vector):
|
111
|
+
```ruby
|
112
|
+
def self.dollars(str); BigDecimal.new(str); end
|
113
|
+
|
114
|
+
market_block = Hamster.from(
|
115
|
+
{
|
116
|
+
market_id: 42,
|
117
|
+
market_name: "The Restaurant at the End of the Universe",
|
118
|
+
account_dropdown: {
|
119
|
+
"Hotblack Desiato" => 1,
|
120
|
+
"Zaphod Beeblebrox" => 3
|
121
|
+
},
|
122
|
+
order_rows: [
|
123
|
+
{ order_id: 101, order_number: "MILLIWAYS-00101", order_totals: { gross: dollars("120"), tax: dollars("14.4"), fee: dollars("20"), net: dollars("85.6") } },
|
124
|
+
{ order_id: 102, order_number: "MILLIWAYS-00102", order_totals: { gross: dollars("3030"), tax: dollars("363.6"), fee: dollars("505.10"), net: dollars("2161.3") } },
|
125
|
+
],
|
126
|
+
market_totals: { gross: dollars("3150"), tax: dollars("378"), fee: dollars("525.10"), net: dollars("2246.9") }
|
127
|
+
}
|
128
|
+
)
|
129
|
+
```
|
130
|
+
|
131
|
+
The validation:
|
132
|
+
```ruby
|
133
|
+
RSchema.validate!(OrderReport::HamsterSchema::MarketBlock, market_block)
|
134
|
+
```
|
49
135
|
|
50
136
|
[HAMSTER-DOC]: https://github.com/hamstergem/hamster
|
51
137
|
[RSCHEMA-DOC]: https://github.com/tomdalling/rschema
|
data/lib/rschema_hamster.rb
CHANGED
@@ -1,99 +1,10 @@
|
|
1
1
|
require 'rschema'
|
2
2
|
require 'hamster'
|
3
|
+
require 'rschema_hamster/hamster_ext'
|
3
4
|
require 'rschema_hamster/dsl'
|
4
5
|
|
5
|
-
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
if not value.is_a?(Hamster::Vector)
|
10
|
-
RSchema::ErrorDetails.new(value, 'is not a Hamster::Vector')
|
11
|
-
elsif fixed_size && value.size != self.size
|
12
|
-
RSchema::ErrorDetails.new(value, "does not have #{self.size} elements")
|
13
|
-
else
|
14
|
-
value.map.with_index do |subvalue, idx|
|
15
|
-
subschema = (fixed_size ? self[idx] : first)
|
16
|
-
subvalue_walked, error = RSchema.walk(subschema, subvalue, mapper)
|
17
|
-
break error.extend_key_path(idx) if error
|
18
|
-
subvalue_walked
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class Hamster::Cons
|
25
|
-
def schema_walk(value, mapper)
|
26
|
-
fixed_size = (self.size != 1)
|
27
|
-
|
28
|
-
if value == Hamster::EmptyList and !fixed_size
|
29
|
-
value
|
30
|
-
elsif not value.is_a?(Hamster::Cons)
|
31
|
-
RSchema::ErrorDetails.new(value, 'is not a Hamster List')
|
32
|
-
elsif fixed_size && value.size != self.size
|
33
|
-
RSchema::ErrorDetails.new(value, "does not have #{self.size} elements")
|
34
|
-
else
|
35
|
-
result = Hamster.list
|
36
|
-
failure = nil
|
37
|
-
value.each.with_index do |subvalue, idx|
|
38
|
-
subschema = (fixed_size ? self[idx] : first)
|
39
|
-
subvalue_walked, error = RSchema.walk(subschema, subvalue, mapper)
|
40
|
-
if error
|
41
|
-
error.extend_key_path(idx)
|
42
|
-
failure = error
|
43
|
-
break
|
44
|
-
else
|
45
|
-
result = result.cons(subvalue_walked)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
return (failure or result.reverse)
|
49
|
-
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
class Hamster::Hash
|
55
|
-
def schema_walk(value, mapper)
|
56
|
-
return RSchema::ErrorDetails.new(value, 'is not a Hash') if not value.is_a?(Hamster::Hash)
|
57
|
-
|
58
|
-
# extract details from the schema
|
59
|
-
required_keys = Set.new
|
60
|
-
all_subschemas = {}
|
61
|
-
each do |(k, subschema)|
|
62
|
-
if k.is_a?(RSchema::OptionalHashKey)
|
63
|
-
all_subschemas[k.key] = subschema
|
64
|
-
else
|
65
|
-
required_keys << k
|
66
|
-
all_subschemas[k] = subschema
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
# check for extra keys that shouldn't be there
|
71
|
-
extraneous = value.keys.reject{ |k| all_subschemas.has_key?(k) }
|
72
|
-
if extraneous.size > 0
|
73
|
-
return RSchema::ErrorDetails.new(value, "has extraneous keys: #{extraneous.to_a.inspect}")
|
74
|
-
end
|
75
|
-
|
76
|
-
# check for required keys that are missing
|
77
|
-
missing_requireds = required_keys.reject{ |k| value.has_key?(k) }
|
78
|
-
if missing_requireds.size > 0
|
79
|
-
return RSchema::ErrorDetails.new(value, "is missing required keys: #{missing_requireds.to_a.inspect}")
|
80
|
-
end
|
81
|
-
|
82
|
-
# walk the subvalues
|
83
|
-
value.reduce(Hamster.hash) do |accum, (k, subvalue)|
|
84
|
-
# puts "RSchema.walk(all_subschemas[k], subvalue, mapper) all_subschemas[#{k.inspect}]: #{all_subschemas[k].inspect}, subvalue: (#{subvalue.class}) #{subvalue.inspect}"
|
85
|
-
subvalue_walked, error = RSchema.walk(all_subschemas[k], subvalue, mapper)
|
86
|
-
# puts " => subvalue_walked: (#{subvalue_walked.class}) #{subvalue_walked.inspect}, error: #{error.inspect}"
|
87
|
-
break error.extend_key_path(k) if error
|
88
|
-
a = accum.put(k, subvalue_walked)
|
89
|
-
# puts " (accum: #{accum.inspect})"
|
90
|
-
a
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
module RSchema
|
96
|
-
module DSL
|
97
|
-
extend RSchemaHamster::DSL
|
6
|
+
module RSchemaHamster
|
7
|
+
def self.schema(dsl=RSchemaHamster::DSL, &block)
|
8
|
+
RSchema.schema(dsl, &block)
|
98
9
|
end
|
99
10
|
end
|
data/lib/rschema_hamster/dsl.rb
CHANGED
@@ -1,15 +1,19 @@
|
|
1
1
|
module RSchemaHamster
|
2
2
|
module DSL
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Base
|
4
|
+
def hamster_hash_of(subschemas_hash)
|
5
|
+
raise InvalidSchemaError unless subschemas_hash.size == 1
|
6
|
+
GenericHamsterHashSchema.new(
|
7
|
+
subschemas_hash.keys.first,
|
8
|
+
subschemas_hash.values.first)
|
9
|
+
end
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
def hamster_set_of(subschema)
|
12
|
+
GenericHamsterSetSchema.new(subschema)
|
13
|
+
end
|
12
14
|
end
|
15
|
+
extend Base
|
16
|
+
extend RSchema::DSL::Base # bring along RSchema's maybe, hash_of, set_of etc.
|
13
17
|
end
|
14
18
|
|
15
19
|
GenericHamsterHashSchema = Struct.new(:key_subschema, :value_subschema) do
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rschema_hamster'
|
2
|
+
|
3
|
+
# 'require' this file to extend RSchema's DSL with RSchema-Hamster's own DSL methods.
|
4
|
+
# Provides more convenient access to the full set of DSL methods (hash_of AND hamster_hash_of, etc.) by typing
|
5
|
+
# RSchema.schema { ... }
|
6
|
+
# instead of
|
7
|
+
# RSchema.schema(RSchemaHamster::DSL) { ... }
|
8
|
+
# ...at the expense of monkey-patching RSchema's DSL module, which shouldn't really cause problems, but would be rude to presume on.
|
9
|
+
module RSchema
|
10
|
+
module DSL
|
11
|
+
extend RSchemaHamster::DSL::Base
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class Hamster::Vector
|
2
|
+
def schema_walk(value, mapper)
|
3
|
+
fixed_size = (self.size != 1)
|
4
|
+
|
5
|
+
if not value.is_a?(Hamster::Vector)
|
6
|
+
RSchema::ErrorDetails.new(value, 'is not a Hamster::Vector')
|
7
|
+
elsif fixed_size && value.size != self.size
|
8
|
+
RSchema::ErrorDetails.new(value, "does not have #{self.size} elements")
|
9
|
+
else
|
10
|
+
value.map.with_index do |subvalue, idx|
|
11
|
+
subschema = (fixed_size ? self[idx] : first)
|
12
|
+
subvalue_walked, error = RSchema.walk(subschema, subvalue, mapper)
|
13
|
+
break error.extend_key_path(idx) if error
|
14
|
+
subvalue_walked
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Hamster::Cons
|
21
|
+
def schema_walk(value, mapper)
|
22
|
+
fixed_size = (self.size != 1)
|
23
|
+
|
24
|
+
if value == Hamster::EmptyList and !fixed_size
|
25
|
+
value
|
26
|
+
elsif not value.is_a?(Hamster::Cons)
|
27
|
+
RSchema::ErrorDetails.new(value, 'is not a Hamster List')
|
28
|
+
elsif fixed_size && value.size != self.size
|
29
|
+
RSchema::ErrorDetails.new(value, "does not have #{self.size} elements")
|
30
|
+
else
|
31
|
+
result = Hamster.list
|
32
|
+
failure = nil
|
33
|
+
value.each.with_index do |subvalue, idx|
|
34
|
+
subschema = (fixed_size ? self[idx] : first)
|
35
|
+
subvalue_walked, error = RSchema.walk(subschema, subvalue, mapper)
|
36
|
+
if error
|
37
|
+
error.extend_key_path(idx)
|
38
|
+
failure = error
|
39
|
+
break
|
40
|
+
else
|
41
|
+
result = result.cons(subvalue_walked)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
return (failure or result.reverse)
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Hamster::Hash
|
51
|
+
def schema_walk(value, mapper)
|
52
|
+
return RSchema::ErrorDetails.new(value, 'is not a Hash') if not value.is_a?(Hamster::Hash)
|
53
|
+
|
54
|
+
# extract details from the schema
|
55
|
+
required_keys = Set.new
|
56
|
+
all_subschemas = {}
|
57
|
+
each do |(k, subschema)|
|
58
|
+
if k.is_a?(RSchema::OptionalHashKey)
|
59
|
+
all_subschemas[k.key] = subschema
|
60
|
+
else
|
61
|
+
required_keys << k
|
62
|
+
all_subschemas[k] = subschema
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# check for extra keys that shouldn't be there
|
67
|
+
extraneous = value.keys.reject{ |k| all_subschemas.has_key?(k) }
|
68
|
+
if extraneous.size > 0
|
69
|
+
return RSchema::ErrorDetails.new(value, "has extraneous keys: #{extraneous.to_a.inspect}")
|
70
|
+
end
|
71
|
+
|
72
|
+
# check for required keys that are missing
|
73
|
+
missing_requireds = required_keys.reject{ |k| value.has_key?(k) }
|
74
|
+
if missing_requireds.size > 0
|
75
|
+
return RSchema::ErrorDetails.new(value, "is missing required keys: #{missing_requireds.to_a.inspect}")
|
76
|
+
end
|
77
|
+
|
78
|
+
# walk the subvalues
|
79
|
+
value.reduce(Hamster.hash) do |accum, (k, subvalue)|
|
80
|
+
# puts "RSchema.walk(all_subschemas[k], subvalue, mapper) all_subschemas[#{k.inspect}]: #{all_subschemas[k].inspect}, subvalue: (#{subvalue.class}) #{subvalue.inspect}"
|
81
|
+
subvalue_walked, error = RSchema.walk(all_subschemas[k], subvalue, mapper)
|
82
|
+
# puts " => subvalue_walked: (#{subvalue_walked.class}) #{subvalue_walked.inspect}, error: #{error.inspect}"
|
83
|
+
break error.extend_key_path(k) if error
|
84
|
+
a = accum.put(k, subvalue_walked)
|
85
|
+
# puts " (accum: #{accum.inspect})"
|
86
|
+
a
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rschema_hamster
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Crosby
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-06-
|
11
|
+
date: 2015-06-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hamster
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '10'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: awesome_print
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
@@ -108,8 +108,10 @@ files:
|
|
108
108
|
- Readme.md
|
109
109
|
- lib/rschema_hamster.rb
|
110
110
|
- lib/rschema_hamster/dsl.rb
|
111
|
+
- lib/rschema_hamster/extend_rschema_dsl.rb
|
112
|
+
- lib/rschema_hamster/hamster_ext.rb
|
111
113
|
- lib/rschema_hamster/version.rb
|
112
|
-
homepage: https://github.com/dcrosby42
|
114
|
+
homepage: https://github.com/dcrosby42/rschema_hamster
|
113
115
|
licenses:
|
114
116
|
- MIT
|
115
117
|
metadata: {}
|