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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d51cbfa9644a3a7c41cddbda5e9487687cb7e550
4
- data.tar.gz: 9447762eb704849daa902bd3fd7dabad102bc04f
3
+ metadata.gz: 02922c410a8a09d62e7ab3b3484d262dbd1aa020
4
+ data.tar.gz: 1ff95e47d3a34bb1442770a815837615ff3fa32e
5
5
  SHA512:
6
- metadata.gz: 2622279af6818f76227d8654414bef9a1dd69dfd9081525617398792a6a14b3ead95330c8af3d66db17fa8b852dc575b70c8a4d01a286e30c115a1f7ad0706b6
7
- data.tar.gz: c21f7acd1cfb2c2583efebd81fa11ccef567da0fa92146a754a851c72a0d95eb9c9d114efc908550c7fb4b30d0550fc08bbca899123836206f68f31db3dc0c6f
6
+ metadata.gz: df924a013064ef6c5e6035959d51facb4e558215aef5b01d8fb796524a06308b9313bc1507902d41772d8ef49368fa077a3983574f12025a5c1a4c130b945171
7
+ data.tar.gz: 98663647b0b6e0947ad26de724ad2ba504532bfed980772946ad8e618a9e877f2b5efb1d79b0fdc048f3483f2025e8a64aef43cdbe7e124dcdb6e60ce773eead
data/Readme.md CHANGED
@@ -1,51 +1,137 @@
1
1
  # RSchema-Hamster
2
2
 
3
- Extends [RSchema][RSCHEMA-DOC] to support [Hamster's][HAMSTER-DOC] immutable Hash, Vector, List and Set structures.
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
- ```ruby
6
- Email = String
7
- PostalCode = String
5
+ ### Baseline
8
6
 
9
- Person = Hamster.hash(
10
- name: String,
11
- email: Email
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: PostalCode
20
+ postal_code: Integer
17
21
  )
18
22
 
19
- Contact = Hamster.hash(
20
- person: Person,
21
- addresses: Hamster.vector(Address)
22
- )
23
+ RSchema.validate!(Address, Hamster.hash(street: "187 Drury Ln", postal_code: 1234))
24
+ ```
23
25
 
24
- ContactList = Hamster.vector(Contact)
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
- DONE:
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
- * Hamster::Vector
30
- * Hamster::Hash - known keys
31
- * Hamster::List
32
- * Test DSL:
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
- TODO:
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
- Implement someday:
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
- * Structs
48
- * Hamsterdam::Struct
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
@@ -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
- class Hamster::Vector
6
- def schema_walk(value, mapper)
7
- fixed_size = (self.size != 1)
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
@@ -1,15 +1,19 @@
1
1
  module RSchemaHamster
2
2
  module DSL
3
- def hamster_hash_of(subschemas_hash)
4
- raise InvalidSchemaError unless subschemas_hash.size == 1
5
- GenericHamsterHashSchema.new(
6
- subschemas_hash.keys.first,
7
- subschemas_hash.values.first)
8
- end
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
- def hamster_set_of(subschema)
11
- GenericHamsterSetSchema.new(subschema)
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
@@ -1,3 +1,3 @@
1
1
  module RSchemaHamster
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.0'
3
3
  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.2
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-03 00:00:00.000000000 Z
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: awesom_print
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: {}