rschema_hamster 0.0.2 → 0.1.0

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 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: {}