membrane 0.0.1
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 +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
|