membrane 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ module Membrane
2
+ class SchemaValidationError < StandardError; end
3
+ end
@@ -0,0 +1,13 @@
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::Any < Membrane::Schema::Base
10
+ def validate(object)
11
+ nil
12
+ end
13
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Membrane
2
+ VERSION = "0.0.1"
3
+ end
data/lib/membrane.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "membrane/errors"
2
+ require "membrane/schema"
3
+ require "membrane/schema_parser"
4
+ require "membrane/version"
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