membrane 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/LICENSE +7136 -0
- data/README.md +90 -0
- data/Rakefile +14 -0
- data/lib/membrane/errors.rb +3 -0
- data/lib/membrane/schema/any.rb +13 -0
- data/lib/membrane/schema/base.rb +17 -0
- data/lib/membrane/schema/bool.rb +20 -0
- data/lib/membrane/schema/class.rb +24 -0
- data/lib/membrane/schema/dictionary.rb +40 -0
- data/lib/membrane/schema/enum.rb +30 -0
- data/lib/membrane/schema/list.rb +37 -0
- data/lib/membrane/schema/record.rb +45 -0
- data/lib/membrane/schema/regexp.rb +29 -0
- data/lib/membrane/schema/tuple.rb +48 -0
- data/lib/membrane/schema/value.rb +22 -0
- data/lib/membrane/schema.rb +17 -0
- data/lib/membrane/schema_parser.rb +186 -0
- data/lib/membrane/version.rb +3 -0
- data/lib/membrane.rb +4 -0
- data/membrane.gemspec +25 -0
- data/spec/any_schema_spec.rb +14 -0
- data/spec/bool_schema_spec.rb +18 -0
- data/spec/class_schema_spec.rb +22 -0
- data/spec/complex_schema_spec.rb +50 -0
- data/spec/dictionary_schema_spec.rb +57 -0
- data/spec/enum_schema_spec.rb +17 -0
- data/spec/list_schema_spec.rb +46 -0
- data/spec/record_schema_spec.rb +56 -0
- data/spec/regexp_schema_spec.rb +19 -0
- data/spec/schema_parser_spec.rb +249 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/tuple_schema_spec.rb +29 -0
- data/spec/value_schema_spec.rb +16 -0
- metadata +142 -0
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Membrane
|
2
|
+
|
3
|
+
Membrane provides an easy to use DSL for specifying validators declaratively.
|
4
|
+
It's intended to be used to validate data received from external sources,
|
5
|
+
such as API endpoints or config files. Use it at the edges of your process to
|
6
|
+
decide what data to let in and what to keep out.
|
7
|
+
|
8
|
+
## Overview
|
9
|
+
|
10
|
+
The core concept behind Membrane is the ```schema```. A ```schema```
|
11
|
+
represents an invariant about a piece of data (similar to a type) and is
|
12
|
+
capable of verifying whether or not a supplied datum satisfies the
|
13
|
+
invariant. Schemas may be composed together to produce more expressive
|
14
|
+
constructs.
|
15
|
+
|
16
|
+
Membrane provides a handful of useful schemas out of the box. You should be
|
17
|
+
able to construct the majority of your schemas using only what is provided
|
18
|
+
by default. The provided schemas are:
|
19
|
+
|
20
|
+
* _Any_ - Accepts all values. Use it sparingly.
|
21
|
+
* _Bool_ - Accepts ```true``` and ```false```.
|
22
|
+
* _Class_ - Accepts instances of a supplied class.
|
23
|
+
* _Dictionary_ - Accepts hashes whose keys and values validate against their
|
24
|
+
respective schemas.
|
25
|
+
* _Enum_ - Accepts values that validate against _any_ of the supplied
|
26
|
+
schemas. Similar to a sum type.
|
27
|
+
* _List_ - Accepts arrays where each element of the array validates
|
28
|
+
against a supplied schema.
|
29
|
+
* _Record_ - Accepts hashes with known keys. Each key has a supplied schema,
|
30
|
+
against which its value must validate.
|
31
|
+
* _Regexp_ - Accepts strings that match a supplied regular expression.
|
32
|
+
* _Tuple_ - Accepts arrays of a given length whose elements validate
|
33
|
+
against their respective schemas.
|
34
|
+
* _Value_ - Accepts values using ```==```.
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Membrane schemas are typically created using a concise DSL. For example, the
|
39
|
+
following creates a schema that will validate a hash where the key "ints"
|
40
|
+
maps to a list of integers and the key "string" maps to a string.
|
41
|
+
|
42
|
+
schema = Membrane::SchemaParser.parse do
|
43
|
+
{ "ints" => [Integer],
|
44
|
+
"string" => String,
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
# Validates successfully
|
49
|
+
schema.validate({
|
50
|
+
"ints" => [1],
|
51
|
+
"string" => "hi",
|
52
|
+
})
|
53
|
+
|
54
|
+
# Fails validation. The key "string" is missing and the value for "ints"
|
55
|
+
# isn't the correct type.
|
56
|
+
schema.validate({
|
57
|
+
"ints" => "invalid",
|
58
|
+
})
|
59
|
+
|
60
|
+
This is a more complicated example that illustrate the entire DSL. It should
|
61
|
+
be self-explanatory.
|
62
|
+
|
63
|
+
Membrane::SchemaParser.parse do
|
64
|
+
{ "ints" => [Integer]
|
65
|
+
"true_or_false" => bool,
|
66
|
+
"anything" => any,
|
67
|
+
optional("_") => any,
|
68
|
+
"one_or_two" => enum(1, 2),
|
69
|
+
"strs_to_ints" => dict(String, Integer),
|
70
|
+
"foo_prefix" => /^foo/,
|
71
|
+
"three_ints" => tuple(Integer, Integer, Integer),
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
## Adding new schemas
|
76
|
+
|
77
|
+
Adding a new schema is trivial. Any class implementing the following "interface"
|
78
|
+
can be used as a schema:
|
79
|
+
|
80
|
+
# @param [Object] The object being validated.
|
81
|
+
#
|
82
|
+
# @raise [Membrane::SchemaValidationError] Raised when a supplied object is
|
83
|
+
# invalid.
|
84
|
+
#
|
85
|
+
# @return [nil]
|
86
|
+
def validate(object)
|
87
|
+
|
88
|
+
If you wish to include your new schema as part of the DSL, you'll need to
|
89
|
+
modify ```membrane/schema_parser.rb``` and have your class inherit from
|
90
|
+
```Membrane::Schema::Base```
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "ci/reporter/rake/rspec"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
desc "Run all specs"
|
6
|
+
RSpec::Core::RakeTask.new("spec") do |t|
|
7
|
+
t.rspec_opts = %w[--color --format documentation]
|
8
|
+
end
|
9
|
+
|
10
|
+
desc "Run all specs and provide output for ci"
|
11
|
+
RSpec::Core::RakeTask.new("spec:ci" => "ci:setup:rspec") do |t|
|
12
|
+
t.rspec_opts = %w[--no-color --format documentation]
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Membrane
|
2
|
+
module Schema
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
class Membrane::Schema::Base
|
7
|
+
# Verifies whether or not the supplied object conforms to this schema
|
8
|
+
#
|
9
|
+
# @param [Object] The object being validated
|
10
|
+
#
|
11
|
+
# @raise [Membrane::SchemaValidationError]
|
12
|
+
#
|
13
|
+
# @return [nil]
|
14
|
+
def validate(object)
|
15
|
+
raise NotImplementedError
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "membrane/errors"
|
4
|
+
require "membrane/schema/base"
|
5
|
+
|
6
|
+
module Membrane
|
7
|
+
module Schema
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Membrane::Schema::Bool < Membrane::Schema::Base
|
12
|
+
TRUTH_VALUES = Set.new([true, false])
|
13
|
+
|
14
|
+
def validate(object)
|
15
|
+
if !TRUTH_VALUES.include?(object)
|
16
|
+
emsg = "Expected instance of true or false, given #{object}"
|
17
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::Class < Membrane::Schema::Base
|
10
|
+
attr_reader :klass
|
11
|
+
|
12
|
+
def initialize(klass)
|
13
|
+
@klass = klass
|
14
|
+
end
|
15
|
+
|
16
|
+
# Validates whether or not the supplied object is derived from klass
|
17
|
+
def validate(object)
|
18
|
+
if !object.kind_of?(@klass)
|
19
|
+
emsg = "Expected instance of #{@klass}," \
|
20
|
+
+ " given an instance of #{object.class}"
|
21
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::Dictionary
|
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
|
+
if !object.kind_of?(Hash)
|
20
|
+
emsg = "Expected instance of Hash, given instance of #{object.class}."
|
21
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
22
|
+
end
|
23
|
+
|
24
|
+
errors = {}
|
25
|
+
|
26
|
+
object.each do |k, v|
|
27
|
+
begin
|
28
|
+
@key_schema.validate(k)
|
29
|
+
@value_schema.validate(v)
|
30
|
+
rescue Membrane::SchemaValidationError => e
|
31
|
+
errors[k] = e.to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
if errors.size > 0
|
36
|
+
emsg = "{ " + errors.map { |k, e| "#{k} => #{e}" }.join(", ") + " }"
|
37
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::Enum < Membrane::Schema::Base
|
10
|
+
attr_reader :elem_schemas
|
11
|
+
|
12
|
+
def initialize(*elem_schemas)
|
13
|
+
@elem_schemas = elem_schemas
|
14
|
+
@elem_schema_str = elem_schemas.map { |s| s.to_s }.join(", ")
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(object)
|
18
|
+
@elem_schemas.each do |schema|
|
19
|
+
begin
|
20
|
+
schema.validate(object)
|
21
|
+
return nil
|
22
|
+
rescue Membrane::SchemaValidationError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
emsg = "Object #{object} doesn't validate" \
|
27
|
+
+ " against any of #{@elem_schema_str}"
|
28
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::List < Membrane::Schema::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
|
+
if !object.kind_of?(Array)
|
18
|
+
emsg = "Expected instance of Array, given instance of #{object.class}"
|
19
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
20
|
+
end
|
21
|
+
|
22
|
+
errors = {}
|
23
|
+
|
24
|
+
object.each_with_index do |elem, ii|
|
25
|
+
begin
|
26
|
+
@elem_schema.validate(elem)
|
27
|
+
rescue Membrane::SchemaValidationError => e
|
28
|
+
errors[ii] = e.to_s
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
if errors.size > 0
|
33
|
+
emsg = errors.map { |ii, e| "At index #{ii}: #{e}" }.join(", ")
|
34
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require "membrane/errors"
|
4
|
+
require "membrane/schema/base"
|
5
|
+
|
6
|
+
module Membrane
|
7
|
+
module Schema
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Membrane::Schema::Record < Membrane::Schema::Base
|
12
|
+
attr_reader :schemas
|
13
|
+
attr_reader :optional_keys
|
14
|
+
|
15
|
+
def initialize(schemas, optional_keys = [])
|
16
|
+
@optional_keys = Set.new(optional_keys)
|
17
|
+
@schemas = schemas
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate(object)
|
21
|
+
unless object.kind_of?(Hash)
|
22
|
+
emsg = "Expected instance of Hash, given instance of #{object.class}"
|
23
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
24
|
+
end
|
25
|
+
|
26
|
+
key_errors = {}
|
27
|
+
|
28
|
+
@schemas.each do |k, schema|
|
29
|
+
if object.has_key?(k)
|
30
|
+
begin
|
31
|
+
schema.validate(object[k])
|
32
|
+
rescue Membrane::SchemaValidationError => e
|
33
|
+
key_errors[k] = e.to_s
|
34
|
+
end
|
35
|
+
elsif !@optional_keys.include?(k)
|
36
|
+
key_errors[k] = "Missing key"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if key_errors.size > 0
|
41
|
+
emsg = "{ " + key_errors.map { |k, e| "#{k} => #{e}" }.join(", ") + " }"
|
42
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::Regexp < Membrane::Schema::Base
|
10
|
+
attr_reader :regexp
|
11
|
+
|
12
|
+
def initialize(regexp)
|
13
|
+
@regexp = regexp
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(object)
|
17
|
+
if !object.kind_of?(String)
|
18
|
+
emsg = "Expected instance of String, given instance of #{object.class}"
|
19
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
20
|
+
end
|
21
|
+
|
22
|
+
if !regexp.match(object)
|
23
|
+
emsg = "Value #{object} doesn't match regexp #{@regexp}"
|
24
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
25
|
+
end
|
26
|
+
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::Tuple < Membrane::Schema::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
|
+
if !object.kind_of?(Array)
|
18
|
+
emsg = "Expected instance of Array, given instance of #{object.class}"
|
19
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
20
|
+
end
|
21
|
+
|
22
|
+
expected = @elem_schemas.length
|
23
|
+
actual = object.length
|
24
|
+
|
25
|
+
if actual != expected
|
26
|
+
emsg = "Expected #{expected} element(s), given #{actual}"
|
27
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
28
|
+
end
|
29
|
+
|
30
|
+
errors = {}
|
31
|
+
|
32
|
+
@elem_schemas.each_with_index do |schema, ii|
|
33
|
+
begin
|
34
|
+
schema.validate(object[ii])
|
35
|
+
rescue Membrane::SchemaValidationError => e
|
36
|
+
errors[ii] = e
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if errors.size > 0
|
41
|
+
emsg = "There were errors at the following indices: " \
|
42
|
+
+ errors.map { |ii, err| "#{ii} => #{err}" }.join(", ")
|
43
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
44
|
+
end
|
45
|
+
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "membrane/errors"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
|
4
|
+
module Membrane
|
5
|
+
module Schema
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Membrane::Schema::Value < Membrane::Schema::Base
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
def initialize(value)
|
13
|
+
@value = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate(object)
|
17
|
+
if object != @value
|
18
|
+
emsg = "Expected #{@value}, given #{object}"
|
19
|
+
raise Membrane::SchemaValidationError.new(emsg)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "membrane/schema/any"
|
2
|
+
require "membrane/schema/base"
|
3
|
+
require "membrane/schema/bool"
|
4
|
+
require "membrane/schema/class"
|
5
|
+
require "membrane/schema/dictionary"
|
6
|
+
require "membrane/schema/enum"
|
7
|
+
require "membrane/schema/list"
|
8
|
+
require "membrane/schema/record"
|
9
|
+
require "membrane/schema/regexp"
|
10
|
+
require "membrane/schema/tuple"
|
11
|
+
require "membrane/schema/value"
|
12
|
+
|
13
|
+
module Membrane
|
14
|
+
module Schema
|
15
|
+
ANY = Membrane::Schema::Any.new
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "membrane/schema"
|
2
|
+
|
3
|
+
module Membrane
|
4
|
+
end
|
5
|
+
|
6
|
+
class Membrane::SchemaParser
|
7
|
+
DEPARSE_INDENT = " ".freeze
|
8
|
+
|
9
|
+
class Dsl
|
10
|
+
OptionalKeyMarker = Struct.new(:key)
|
11
|
+
DictionaryMarker = Struct.new(:key_schema, :value_schema)
|
12
|
+
EnumMarker = Struct.new(:elem_schemas)
|
13
|
+
TupleMarker = Struct.new(:elem_schemas)
|
14
|
+
|
15
|
+
def any
|
16
|
+
Membrane::Schema::Any.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def bool
|
20
|
+
Membrane::Schema::Bool.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def enum(*elem_schemas)
|
24
|
+
EnumMarker.new(elem_schemas)
|
25
|
+
end
|
26
|
+
|
27
|
+
def dict(key_schema, value_schema)
|
28
|
+
DictionaryMarker.new(key_schema, value_schema)
|
29
|
+
end
|
30
|
+
|
31
|
+
def optional(key)
|
32
|
+
Dsl::OptionalKeyMarker.new(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
def tuple(*elem_schemas)
|
36
|
+
TupleMarker.new(elem_schemas)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.parse(&blk)
|
41
|
+
new.parse(&blk)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.deparse(schema)
|
45
|
+
new.deparse(schema)
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse(&blk)
|
49
|
+
intermediate_schema = Dsl.new.instance_eval(&blk)
|
50
|
+
|
51
|
+
do_parse(intermediate_schema)
|
52
|
+
end
|
53
|
+
|
54
|
+
def deparse(schema)
|
55
|
+
case schema
|
56
|
+
when Membrane::Schema::Any
|
57
|
+
"any"
|
58
|
+
when Membrane::Schema::Bool
|
59
|
+
"bool"
|
60
|
+
when Membrane::Schema::Class
|
61
|
+
schema.klass.inspect
|
62
|
+
when Membrane::Schema::Dictionary
|
63
|
+
"dict(%s, %s)" % [deparse(schema.key_schema),
|
64
|
+
deparse(schema.value_schema)]
|
65
|
+
when Membrane::Schema::Enum
|
66
|
+
"enum(%s)" % [schema.elem_schemas.map { |es| deparse(es) }.join(", ")]
|
67
|
+
when Membrane::Schema::List
|
68
|
+
"[%s]" % [deparse(schema.elem_schema)]
|
69
|
+
when Membrane::Schema::Record
|
70
|
+
deparse_record(schema)
|
71
|
+
when Membrane::Schema::Regexp
|
72
|
+
schema.regexp.inspect
|
73
|
+
when Membrane::Schema::Tuple
|
74
|
+
"tuple(%s)" % [schema.elem_schemas.map { |es| deparse(es) }.join(", ")]
|
75
|
+
when Membrane::Schema::Value
|
76
|
+
schema.value.inspect
|
77
|
+
when Membrane::Schema::Base
|
78
|
+
if schema.respond_to?(:deparse)
|
79
|
+
schema.deparse
|
80
|
+
else
|
81
|
+
schema.inspect
|
82
|
+
end
|
83
|
+
else
|
84
|
+
emsg = "Expected instance of Membrane::Schema::Base, given instance of" \
|
85
|
+
+ " #{schema.class}"
|
86
|
+
raise ArgumentError.new(emsg)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def do_parse(object)
|
93
|
+
case object
|
94
|
+
when Hash
|
95
|
+
parse_record(object)
|
96
|
+
when Array
|
97
|
+
parse_list(object)
|
98
|
+
when Class
|
99
|
+
Membrane::Schema::Class.new(object)
|
100
|
+
when Regexp
|
101
|
+
Membrane::Schema::Regexp.new(object)
|
102
|
+
when Dsl::DictionaryMarker
|
103
|
+
Membrane::Schema::Dictionary.new(do_parse(object.key_schema),
|
104
|
+
do_parse(object.value_schema))
|
105
|
+
when Dsl::EnumMarker
|
106
|
+
elem_schemas = object.elem_schemas.map { |s| do_parse(s) }
|
107
|
+
Membrane::Schema::Enum.new(*elem_schemas)
|
108
|
+
when Dsl::TupleMarker
|
109
|
+
elem_schemas = object.elem_schemas.map { |s| do_parse(s) }
|
110
|
+
Membrane::Schema::Tuple.new(*elem_schemas)
|
111
|
+
when Membrane::Schema::Base
|
112
|
+
object
|
113
|
+
else
|
114
|
+
Membrane::Schema::Value.new(object)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_list(schema)
|
119
|
+
if schema.empty?
|
120
|
+
raise ArgumentError.new("You must supply a schema for elements.")
|
121
|
+
elsif schema.length > 1
|
122
|
+
raise ArgumentError.new("Lists can only match a single schema.")
|
123
|
+
end
|
124
|
+
|
125
|
+
Membrane::Schema::List.new(do_parse(schema[0]))
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_record(schema)
|
129
|
+
if schema.empty?
|
130
|
+
raise ArgumentError.new("You must supply at least one key-value pair.")
|
131
|
+
end
|
132
|
+
|
133
|
+
optional_keys = []
|
134
|
+
|
135
|
+
parsed = {}
|
136
|
+
|
137
|
+
schema.each do |key, value_schema|
|
138
|
+
if key.kind_of?(Dsl::OptionalKeyMarker)
|
139
|
+
key = key.key
|
140
|
+
optional_keys << key
|
141
|
+
end
|
142
|
+
|
143
|
+
parsed[key] = do_parse(value_schema)
|
144
|
+
end
|
145
|
+
|
146
|
+
Membrane::Schema::Record.new(parsed, optional_keys)
|
147
|
+
end
|
148
|
+
|
149
|
+
def deparse_record(schema)
|
150
|
+
lines = ["{"]
|
151
|
+
|
152
|
+
schema.schemas.each do |key, val_schema|
|
153
|
+
dep_key = nil
|
154
|
+
if schema.optional_keys.include?(key)
|
155
|
+
dep_key = "optional(%s)" % [key.inspect]
|
156
|
+
else
|
157
|
+
dep_key = key.inspect
|
158
|
+
end
|
159
|
+
|
160
|
+
dep_val_schema_lines = deparse(val_schema).split("\n")
|
161
|
+
|
162
|
+
dep_val_schema_lines.each_with_index do |line, line_idx|
|
163
|
+
to_append = nil
|
164
|
+
|
165
|
+
if 0 == line_idx
|
166
|
+
to_append = "%s => %s" % [dep_key, line]
|
167
|
+
else
|
168
|
+
# Indent continuation lines
|
169
|
+
to_append = DEPARSE_INDENT + line
|
170
|
+
end
|
171
|
+
|
172
|
+
# This concludes the deparsed schema, append a comma in preparation
|
173
|
+
# for the next k-v pair.
|
174
|
+
if dep_val_schema_lines.size - 1 == line_idx
|
175
|
+
to_append += ","
|
176
|
+
end
|
177
|
+
|
178
|
+
lines << to_append
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
lines << "}"
|
183
|
+
|
184
|
+
lines.join("\n")
|
185
|
+
end
|
186
|
+
end
|
data/lib/membrane.rb
ADDED
data/membrane.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/membrane/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = "membrane"
|
6
|
+
gem.version = Membrane::VERSION
|
7
|
+
gem.date = "2012-07-09"
|
8
|
+
gem.summary = "A DSL for validating data."
|
9
|
+
gem.homepage = "http://www.cloudfoundry.org"
|
10
|
+
gem.authors = ["mpage"]
|
11
|
+
gem.email = ["support@cloudfoundry.org"]
|
12
|
+
gem.description =<<-EOT
|
13
|
+
Membrane provides an easy to use DSL for specifying validation
|
14
|
+
logic declaratively.
|
15
|
+
EOT
|
16
|
+
|
17
|
+
gem.files = `git ls-files`.split($\)
|
18
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
19
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
20
|
+
gem.require_paths = ["lib"]
|
21
|
+
|
22
|
+
gem.add_development_dependency("ci_reporter")
|
23
|
+
gem.add_development_dependency("rake")
|
24
|
+
gem.add_development_dependency("rspec")
|
25
|
+
end
|