membrane 0.0.2 → 0.0.4
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.
- data/.gitignore +1 -0
- data/.travis.yml +6 -0
- data/LICENSE +58 -6869
- data/NOTICE +10 -0
- data/README.md +130 -30
- data/lib/membrane/schema_parser.rb +25 -24
- data/lib/membrane/schemas/any.rb +8 -0
- data/lib/membrane/{schema → schemas}/base.rb +1 -6
- data/lib/membrane/schemas/bool.rb +31 -0
- data/lib/membrane/schemas/class.rb +35 -0
- data/lib/membrane/schemas/dictionary.rb +69 -0
- data/lib/membrane/schemas/enum.rb +49 -0
- data/lib/membrane/schemas/list.rb +63 -0
- data/lib/membrane/schemas/record.rb +96 -0
- data/lib/membrane/schemas/regexp.rb +60 -0
- data/lib/membrane/schemas/tuple.rb +90 -0
- data/lib/membrane/schemas/value.rb +37 -0
- data/lib/membrane/schemas.rb +5 -0
- data/lib/membrane/version.rb +1 -1
- data/lib/membrane.rb +1 -1
- data/spec/schema_parser_spec.rb +79 -75
- data/spec/{any_schema_spec.rb → schemas/any_spec.rb} +2 -2
- data/spec/{base_schema_spec.rb → schemas/base_spec.rb} +2 -2
- data/spec/{bool_schema_spec.rb → schemas/bool_spec.rb} +2 -3
- data/spec/{class_schema_spec.rb → schemas/class_spec.rb} +2 -2
- data/spec/{dictionary_schema_spec.rb → schemas/dictionary_spec.rb} +11 -11
- data/spec/{enum_schema_spec.rb → schemas/enum_spec.rb} +4 -4
- data/spec/{list_schema_spec.rb → schemas/list_spec.rb} +8 -8
- data/spec/schemas/record_spec.rb +108 -0
- data/spec/{regexp_schema_spec.rb → schemas/regexp_spec.rb} +2 -2
- data/spec/{tuple_schema_spec.rb → schemas/tuple_spec.rb} +4 -4
- data/spec/{value_schema_spec.rb → schemas/value_spec.rb} +2 -2
- metadata +37 -35
- data/lib/membrane/schema/any.rb +0 -13
- data/lib/membrane/schema/bool.rb +0 -20
- data/lib/membrane/schema/class.rb +0 -24
- data/lib/membrane/schema/dictionary.rb +0 -40
- data/lib/membrane/schema/enum.rb +0 -30
- data/lib/membrane/schema/list.rb +0 -37
- data/lib/membrane/schema/record.rb +0 -45
- data/lib/membrane/schema/regexp.rb +0 -29
- data/lib/membrane/schema/tuple.rb +0 -48
- data/lib/membrane/schema/value.rb +0 -22
- data/lib/membrane/schema.rb +0 -17
- data/spec/record_schema_spec.rb +0 -56
data/NOTICE
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
cf-membrane
|
2
|
+
|
3
|
+
Copyright (c) 2013 Pivotal Software Inc. All Rights Reserved.
|
4
|
+
|
5
|
+
This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
6
|
+
You may not use this product except in compliance with the License.
|
7
|
+
|
8
|
+
This product may include a number of subcomponents with separate copyright notices
|
9
|
+
and license terms. Your use of these subcomponents is subject to the terms and
|
10
|
+
conditions of the subcomponent's license, as noted in the LICENSE file.
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[](https://travis-ci.org/cloudfoundry/membrane)
|
2
|
+
|
1
3
|
# Membrane
|
2
4
|
|
3
5
|
Membrane provides an easy to use DSL for specifying validators declaratively.
|
@@ -7,38 +9,137 @@ decide what data to let in and what to keep out.
|
|
7
9
|
|
8
10
|
## Overview
|
9
11
|
|
10
|
-
The core concept behind Membrane is the ```schema```. A ```schema```
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
constructs.
|
12
|
+
The core concept behind Membrane is the ```schema```. A ```schema``` represents
|
13
|
+
an invariant about a piece of data (similar to a type) and is capable of
|
14
|
+
verifying whether or not a supplied datum satisfies the invariant. Schemas may
|
15
|
+
be composed to produce more expressive constructs.
|
15
16
|
|
16
17
|
Membrane provides a handful of useful schemas out of the box. You should be
|
17
18
|
able to construct the majority of your schemas using only what is provided
|
18
|
-
by default.
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
*
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
*
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
19
|
+
by default.
|
20
|
+
|
21
|
+
|
22
|
+
*Any*
|
23
|
+
|
24
|
+
The ```Any``` schema accepts all values; use it sparingly. It is synonymous to
|
25
|
+
the Object class in Ruby.
|
26
|
+
|
27
|
+
*Bool*
|
28
|
+
|
29
|
+
The ```Bool``` schema accepts only the values ```true``` and ```false```.
|
30
|
+
|
31
|
+
*Class*
|
32
|
+
|
33
|
+
The ```Class``` schema is parameterized by an instance of
|
34
|
+
```Class```. It accepts any values that are instances of the supplied class.
|
35
|
+
This is verified using ```kind_of?```.
|
36
|
+
|
37
|
+
*Dictionary*
|
38
|
+
|
39
|
+
The ```Dictionary``` schema is parameterized by a key schema and a
|
40
|
+
value schema. It accepts hashes whose keys and values validate against their
|
41
|
+
respective schemas.
|
42
|
+
|
43
|
+
*Enum*
|
44
|
+
|
45
|
+
The ```Enum``` parameterized by an arbitrary number of value schemas. It
|
46
|
+
accepts any values that are accepted by at least one of the supplied schemas.
|
47
|
+
|
48
|
+
*List*
|
49
|
+
|
50
|
+
The ```List``` schema is parameterized by a single element schema. It accepts
|
51
|
+
arrays whose elements are accepted by the supplied schema.
|
52
|
+
|
53
|
+
*Record*
|
54
|
+
|
55
|
+
The ```Record``` schema is parameterized by a set of known keys and their
|
56
|
+
respective schemas. It accepts hashes that contain all the supplied keys,
|
57
|
+
assuming the corresponding values are accepted by their respective schemas.
|
58
|
+
|
59
|
+
*Regexp*
|
60
|
+
|
61
|
+
The ```Regexp``` schema is parameterized by a regular expression. It accepts
|
62
|
+
strings that match the supplied regular expression.
|
63
|
+
|
64
|
+
*Tuple*
|
65
|
+
|
66
|
+
The ```Tuple``` schema is parameterized by a fixed number of schemas. It accepts
|
67
|
+
arrays of the same length, where each element is accepted by its associated
|
68
|
+
schema.
|
69
|
+
|
70
|
+
*Value*
|
71
|
+
|
72
|
+
The ```Value``` schema is parameterized by a single value. It accepts values
|
73
|
+
who are equal to the parameterizing value using ```==```.
|
74
|
+
|
75
|
+
## DSL
|
76
|
+
|
77
|
+
Membrane schemas are typically created using a concise DSL. The aforementioned
|
78
|
+
schemas are represented in the DSL as follows:
|
79
|
+
|
80
|
+
*Any*
|
81
|
+
|
82
|
+
The ```Any``` schema is represented by the keyword ```any```.
|
83
|
+
|
84
|
+
*Bool*
|
85
|
+
|
86
|
+
The ```Bool``` schema is represented by the keyword ```bool```.
|
87
|
+
|
88
|
+
*Class*
|
89
|
+
|
90
|
+
The ```Class``` schema is represented by the parameterizing instance of ```Class```.
|
91
|
+
For example, an instance of the Class schema that validates strings would be
|
92
|
+
represented as ```String```.
|
93
|
+
|
94
|
+
*Dictionary*
|
95
|
+
|
96
|
+
The ```Dictionary``` schema is represented by ```dict(key_schema,
|
97
|
+
value_schema```, where ```key_schema``` is the schema used to validate keys,
|
98
|
+
and ```value_schema``` is the schema used to validate values.
|
99
|
+
|
100
|
+
*Enum*
|
101
|
+
|
102
|
+
The ```Enum``` schema is represented by ```enum(schema1, ..., schemaN)```
|
103
|
+
where ```schema1``` through ```schemaN``` are the possible value schemas.
|
104
|
+
|
105
|
+
*List*
|
106
|
+
|
107
|
+
The ```List``` schema is represented by ```[elem_schema]```, where
|
108
|
+
```elem_schema``` is the schema that all list elements must validate against.
|
109
|
+
|
110
|
+
*Record*
|
111
|
+
|
112
|
+
The ```Record``` schema is represented as follows:
|
113
|
+
|
114
|
+
{ "key1" => value1_schema,
|
115
|
+
optional("key2") => value2_schema,
|
116
|
+
...
|
117
|
+
}
|
118
|
+
|
119
|
+
Here ```key1``` must be contained in the hash and the corresponding value must
|
120
|
+
be accepted by ```value1_schema```. Note that ```key2``` is marked as optional.
|
121
|
+
If present, its corresponding value must be accepted by ```value2_schema```.
|
122
|
+
|
123
|
+
*Regexp*
|
124
|
+
|
125
|
+
The ```Regexp``` schema is represented by regexp literals. For example,
|
126
|
+
```/foo|bar/``` matches strings containing "foo" or "bar".
|
127
|
+
|
128
|
+
*Tuple*
|
129
|
+
|
130
|
+
The ```Tuple``` schema is represented as ```tuple(schema0, ..., schemaN)```,
|
131
|
+
where the Ith element of an array must be accepted by ```schemaI```.
|
132
|
+
|
133
|
+
*Value*
|
134
|
+
|
135
|
+
The ```Value``` schema is represented by the value to be validated. For example,
|
136
|
+
```"foo"``` accepts only the string "foo".
|
36
137
|
|
37
138
|
## Usage
|
38
139
|
|
39
|
-
|
40
|
-
following creates a schema that will validate a hash where the
|
41
|
-
maps to a list of integers and the key "string" maps to a string.
|
140
|
+
While the previous section was a bit abstract, the DSL is fairly intuitive.
|
141
|
+
For example, the following creates a schema that will validate a hash where the
|
142
|
+
key "ints" maps to a list of integers and the key "string" maps to a string.
|
42
143
|
|
43
144
|
schema = Membrane::SchemaParser.parse do
|
44
145
|
{ "ints" => [Integer],
|
@@ -58,8 +159,8 @@ maps to a list of integers and the key "string" maps to a string.
|
|
58
159
|
"ints" => "invalid",
|
59
160
|
})
|
60
161
|
|
61
|
-
This is a more complicated example that illustrate the entire DSL.
|
62
|
-
|
162
|
+
This is a more complicated example that illustrate the entire DSL. Hopefully
|
163
|
+
it is self-explanatory:
|
63
164
|
|
64
165
|
Membrane::SchemaParser.parse do
|
65
166
|
{ "ints" => [Integer]
|
@@ -87,5 +188,4 @@ can be used as a schema:
|
|
87
188
|
def validate(object)
|
88
189
|
|
89
190
|
If you wish to include your new schema as part of the DSL, you'll need to
|
90
|
-
modify ```membrane/schema_parser.rb``` and have your class inherit from
|
91
|
-
```Membrane::Schema::Base```
|
191
|
+
modify ```membrane/schema_parser.rb``` and have your class inherit from ```Membrane::Schemas::Base```
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require "membrane/
|
1
|
+
require "membrane/schemas"
|
2
2
|
|
3
3
|
module Membrane
|
4
4
|
end
|
@@ -13,11 +13,11 @@ class Membrane::SchemaParser
|
|
13
13
|
TupleMarker = Struct.new(:elem_schemas)
|
14
14
|
|
15
15
|
def any
|
16
|
-
Membrane::
|
16
|
+
Membrane::Schemas::Any.new
|
17
17
|
end
|
18
18
|
|
19
19
|
def bool
|
20
|
-
Membrane::
|
20
|
+
Membrane::Schemas::Bool.new
|
21
21
|
end
|
22
22
|
|
23
23
|
def enum(*elem_schemas)
|
@@ -53,31 +53,31 @@ class Membrane::SchemaParser
|
|
53
53
|
|
54
54
|
def deparse(schema)
|
55
55
|
case schema
|
56
|
-
when Membrane::
|
56
|
+
when Membrane::Schemas::Any
|
57
57
|
"any"
|
58
|
-
when Membrane::
|
58
|
+
when Membrane::Schemas::Bool
|
59
59
|
"bool"
|
60
|
-
when Membrane::
|
60
|
+
when Membrane::Schemas::Class
|
61
61
|
schema.klass.name
|
62
|
-
when Membrane::
|
62
|
+
when Membrane::Schemas::Dictionary
|
63
63
|
"dict(%s, %s)" % [deparse(schema.key_schema),
|
64
64
|
deparse(schema.value_schema)]
|
65
|
-
when Membrane::
|
65
|
+
when Membrane::Schemas::Enum
|
66
66
|
"enum(%s)" % [schema.elem_schemas.map { |es| deparse(es) }.join(", ")]
|
67
|
-
when Membrane::
|
67
|
+
when Membrane::Schemas::List
|
68
68
|
"[%s]" % [deparse(schema.elem_schema)]
|
69
|
-
when Membrane::
|
69
|
+
when Membrane::Schemas::Record
|
70
70
|
deparse_record(schema)
|
71
|
-
when Membrane::
|
71
|
+
when Membrane::Schemas::Regexp
|
72
72
|
schema.regexp.inspect
|
73
|
-
when Membrane::
|
73
|
+
when Membrane::Schemas::Tuple
|
74
74
|
"tuple(%s)" % [schema.elem_schemas.map { |es| deparse(es) }.join(", ")]
|
75
|
-
when Membrane::
|
75
|
+
when Membrane::Schemas::Value
|
76
76
|
schema.value.inspect
|
77
|
-
when Membrane::
|
77
|
+
when Membrane::Schemas::Base
|
78
78
|
schema.inspect
|
79
79
|
else
|
80
|
-
emsg = "Expected instance of Membrane::
|
80
|
+
emsg = "Expected instance of Membrane::Schemas::Base, given instance of" \
|
81
81
|
+ " #{schema.class}"
|
82
82
|
raise ArgumentError.new(emsg)
|
83
83
|
end
|
@@ -92,22 +92,22 @@ class Membrane::SchemaParser
|
|
92
92
|
when Array
|
93
93
|
parse_list(object)
|
94
94
|
when Class
|
95
|
-
Membrane::
|
95
|
+
Membrane::Schemas::Class.new(object)
|
96
96
|
when Regexp
|
97
|
-
Membrane::
|
97
|
+
Membrane::Schemas::Regexp.new(object)
|
98
98
|
when Dsl::DictionaryMarker
|
99
|
-
Membrane::
|
99
|
+
Membrane::Schemas::Dictionary.new(do_parse(object.key_schema),
|
100
100
|
do_parse(object.value_schema))
|
101
101
|
when Dsl::EnumMarker
|
102
102
|
elem_schemas = object.elem_schemas.map { |s| do_parse(s) }
|
103
|
-
Membrane::
|
103
|
+
Membrane::Schemas::Enum.new(*elem_schemas)
|
104
104
|
when Dsl::TupleMarker
|
105
105
|
elem_schemas = object.elem_schemas.map { |s| do_parse(s) }
|
106
|
-
Membrane::
|
107
|
-
when Membrane::
|
106
|
+
Membrane::Schemas::Tuple.new(*elem_schemas)
|
107
|
+
when Membrane::Schemas::Base
|
108
108
|
object
|
109
109
|
else
|
110
|
-
Membrane::
|
110
|
+
Membrane::Schemas::Value.new(object)
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
@@ -118,7 +118,7 @@ class Membrane::SchemaParser
|
|
118
118
|
raise ArgumentError.new("Lists can only match a single schema.")
|
119
119
|
end
|
120
120
|
|
121
|
-
Membrane::
|
121
|
+
Membrane::Schemas::List.new(do_parse(schema[0]))
|
122
122
|
end
|
123
123
|
|
124
124
|
def parse_record(schema)
|
@@ -139,7 +139,7 @@ class Membrane::SchemaParser
|
|
139
139
|
parsed[key] = do_parse(value_schema)
|
140
140
|
end
|
141
141
|
|
142
|
-
Membrane::
|
142
|
+
Membrane::Schemas::Record.new(parsed, optional_keys)
|
143
143
|
end
|
144
144
|
|
145
145
|
def deparse_record(schema)
|
@@ -153,6 +153,7 @@ class Membrane::SchemaParser
|
|
153
153
|
dep_key = key.inspect
|
154
154
|
end
|
155
155
|
|
156
|
+
dep_key = DEPARSE_INDENT + dep_key
|
156
157
|
dep_val_schema_lines = deparse(val_schema).split("\n")
|
157
158
|
|
158
159
|
dep_val_schema_lines.each_with_index do |line, line_idx|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "membrane/errors"
|
4
|
+
require "membrane/schemas/base"
|
5
|
+
|
6
|
+
class Membrane::Schemas::Bool < Membrane::Schemas::Base
|
7
|
+
def validate(object)
|
8
|
+
BoolValidator.new(object).validate
|
9
|
+
end
|
10
|
+
|
11
|
+
class BoolValidator
|
12
|
+
TRUTH_VALUES = Set.new([true, false])
|
13
|
+
|
14
|
+
def initialize(object)
|
15
|
+
@object = object
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate
|
19
|
+
if !TRUTH_VALUES.include?(@object)
|
20
|
+
fail!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def fail!
|
27
|
+
emsg = "Expected instance of true or false, given #{@object}"
|
28
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schemas/base"
|
3
|
+
|
4
|
+
class Membrane::Schemas::Class < Membrane::Schemas::Base
|
5
|
+
attr_reader :klass
|
6
|
+
|
7
|
+
def initialize(klass)
|
8
|
+
@klass = klass
|
9
|
+
end
|
10
|
+
|
11
|
+
# Validates whether or not the supplied object is derived from klass
|
12
|
+
def validate(object)
|
13
|
+
ClassValidator.new(@klass, object).validate
|
14
|
+
end
|
15
|
+
|
16
|
+
class ClassValidator
|
17
|
+
|
18
|
+
def initialize(klass, object)
|
19
|
+
@klass = klass
|
20
|
+
@object = object
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate
|
24
|
+
fail! if !@object.kind_of?(@klass)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def fail!
|
30
|
+
emsg = "Expected instance of #{@klass}," \
|
31
|
+
+ " given an instance of #{@object.class}"
|
32
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schemas/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schemas::Dictionary < Membrane::Schemas::Base
|
10
|
+
attr_reader :key_schema
|
11
|
+
attr_reader :value_schema
|
12
|
+
|
13
|
+
def initialize(key_schema, value_schema)
|
14
|
+
@key_schema = key_schema
|
15
|
+
@value_schema = value_schema
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate(object)
|
19
|
+
HashValidator.new(object).validate
|
20
|
+
MembersValidator.new(@key_schema, @value_schema, object).validate
|
21
|
+
end
|
22
|
+
|
23
|
+
class HashValidator
|
24
|
+
def initialize(object)
|
25
|
+
@object = object
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate
|
29
|
+
fail!(@object.class) if !@object.kind_of?(Hash)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def fail!(klass)
|
35
|
+
emsg = "Expected instance of Hash, given instance of #{klass}."
|
36
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MembersValidator
|
41
|
+
def initialize(key_schema, value_schema, object)
|
42
|
+
@key_schema = key_schema
|
43
|
+
@value_schema = value_schema
|
44
|
+
@object = object
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate
|
48
|
+
errors = {}
|
49
|
+
|
50
|
+
@object.each do |k, v|
|
51
|
+
begin
|
52
|
+
@key_schema.validate(k)
|
53
|
+
@value_schema.validate(v)
|
54
|
+
rescue Membrane::SchemaValidationError => e
|
55
|
+
errors[k] = e.to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
fail!(errors) if errors.size > 0
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def fail!(errors)
|
65
|
+
emsg = "{ " + errors.map { |k, e| "#{k} => #{e}" }.join(", ") + " }"
|
66
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schemas/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schemas::Enum < Membrane::Schemas::Base
|
10
|
+
attr_reader :elem_schemas
|
11
|
+
|
12
|
+
def initialize(*elem_schemas)
|
13
|
+
@elem_schemas = elem_schemas
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(object)
|
17
|
+
EnumValidator.new(@elem_schemas, object).validate
|
18
|
+
end
|
19
|
+
|
20
|
+
class EnumValidator
|
21
|
+
def initialize(elem_schemas, object)
|
22
|
+
@elem_schemas = elem_schemas
|
23
|
+
@object = object
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate
|
27
|
+
@elem_schemas.each do |schema|
|
28
|
+
begin
|
29
|
+
schema.validate(@object)
|
30
|
+
return nil
|
31
|
+
rescue Membrane::SchemaValidationError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
fail!(@elem_schemas)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def fail!(elem_schemas)
|
41
|
+
elem_schema_str = elem_schemas.map { |s| s.to_s }.join(", ")
|
42
|
+
|
43
|
+
emsg = "Object #{@object} doesn't validate" \
|
44
|
+
+ " against any of #{elem_schema_str}"
|
45
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schemas/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schemas::List < Membrane::Schemas::Base
|
10
|
+
attr_reader :elem_schema
|
11
|
+
|
12
|
+
def initialize(elem_schema)
|
13
|
+
@elem_schema = elem_schema
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(object)
|
17
|
+
ArrayValidator.new(object).validate
|
18
|
+
MemberValidator.new(@elem_schema, object).validate
|
19
|
+
end
|
20
|
+
|
21
|
+
class ArrayValidator
|
22
|
+
def initialize(object)
|
23
|
+
@object = object
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate
|
27
|
+
fail! if !@object.kind_of?(Array)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def fail!
|
33
|
+
emsg = "Expected instance of Array, given instance of #{@object.class}"
|
34
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class MemberValidator
|
39
|
+
def initialize(elem_schema, object)
|
40
|
+
@elem_schema = elem_schema
|
41
|
+
@object = object
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate
|
45
|
+
errors = {}
|
46
|
+
|
47
|
+
@object.each_with_index do |elem, ii|
|
48
|
+
begin
|
49
|
+
@elem_schema.validate(elem)
|
50
|
+
rescue Membrane::SchemaValidationError => e
|
51
|
+
errors[ii] = e.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
fail!(errors) if errors.size > 0
|
56
|
+
end
|
57
|
+
|
58
|
+
def fail!(errors)
|
59
|
+
emsg = errors.map { |ii, e| "At index #{ii}: #{e}" }.join(", ")
|
60
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "membrane/errors"
|
4
|
+
require "membrane/schemas/base"
|
5
|
+
|
6
|
+
module Membrane
|
7
|
+
module Schema
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Membrane::Schemas::Record < Membrane::Schemas::Base
|
12
|
+
attr_reader :schemas
|
13
|
+
attr_reader :optional_keys
|
14
|
+
|
15
|
+
def initialize(schemas, optional_keys = [], strict_checking = false)
|
16
|
+
@optional_keys = Set.new(optional_keys)
|
17
|
+
@schemas = schemas
|
18
|
+
@strict_checking = strict_checking
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate(object)
|
22
|
+
HashValidator.new(object).validate
|
23
|
+
KeyValidator.new(@optional_keys, @schemas, @strict_checking, object).validate
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse(&blk)
|
27
|
+
other_record = Membrane::SchemaParser.parse(&blk)
|
28
|
+
@schemas.merge!(other_record.schemas)
|
29
|
+
@optional_keys << other_record.optional_keys
|
30
|
+
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
class KeyValidator
|
35
|
+
def initialize(optional_keys, schemas, strict_checking, object)
|
36
|
+
@optional_keys = optional_keys
|
37
|
+
@schemas = schemas
|
38
|
+
@strict_checking = strict_checking
|
39
|
+
@object = object
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate
|
43
|
+
key_errors = {}
|
44
|
+
schema_keys = []
|
45
|
+
@schemas.each do |k, schema|
|
46
|
+
if @object.has_key?(k)
|
47
|
+
schema_keys << k
|
48
|
+
begin
|
49
|
+
schema.validate(@object[k])
|
50
|
+
rescue Membrane::SchemaValidationError => e
|
51
|
+
key_errors[k] = e.to_s
|
52
|
+
end
|
53
|
+
elsif !@optional_keys.include?(k)
|
54
|
+
key_errors[k] = "Missing key"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
key_errors.merge!(validate_extra_keys(@object.keys - schema_keys)) if @strict_checking
|
59
|
+
|
60
|
+
fail!(key_errors) if key_errors.size > 0
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def validate_extra_keys(extra_keys)
|
66
|
+
extra_key_errors = {}
|
67
|
+
extra_keys.each do |k|
|
68
|
+
extra_key_errors[k] = "was not specified in the schema"
|
69
|
+
end
|
70
|
+
|
71
|
+
extra_key_errors
|
72
|
+
end
|
73
|
+
|
74
|
+
def fail!(errors)
|
75
|
+
emsg = "{ " + errors.map { |k, e| "#{k} => #{e}" }.join(", ") + " }"
|
76
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class HashValidator
|
81
|
+
def initialize(object)
|
82
|
+
@object = object
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate
|
86
|
+
fail! unless @object.kind_of?(Hash)
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def fail!
|
92
|
+
emsg = "Expected instance of Hash, given instance of #{@object.class}"
|
93
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|