membrane 0.0.2 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/cloudfoundry/membrane.png)](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
|