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/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