Permalink
Browse files

Initial commit of membrane

Membrane is a generalization of earlier work called JsonSchema. It
is more powerful, more thoroughly tested, and has better docs. See
the readme for a more detailed description.

Change-Id: I3ecb2fda299bba0f5899f9b416b7d309debb7ae4
  • Loading branch information...
0 parents commit 5536744e64023a74fcaf346c72b749f7b2719fe4 mpage committed Apr 27, 2012
@@ -0,0 +1,2 @@
+Gemfile.lock
+vendor/cache
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in membrane.gemspec
+gemspec
7,136 LICENSE

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -0,0 +1,83 @@
+# Membrane
+
+Membrane provides an easy to use DSL for specifying validators declaratively.
+It's intended to be used to validate data received from external sources,
+such as API endpoints or config files. Use it at the edges of your process to
+decide what data to let in and what to keep out.
+
+## Overview
+
+The core concept behind Membrane is the ```schema```. A ```schema```
+represents an invariant about a piece of data (similar to a type) and is
+capable of verifying whether or not a supplied datum satisfies the
+invariant. Schemas may be composed together to produce more expressive
+constructs.
+
+Membrane provides a handful of useful schemas out of the box. You should be
+able to construct the majority of your schemas using only what is provided
+by default. The provided schemas are:
+
+* _Any_ - Accepts all values. Use it sparingly.
+* _Bool_ - Accepts ```true``` and ```false```.
+* _Class_ - Accepts instances of a supplied class.
+* _Dictionary_ - Accepts hashes whose keys and values validate against their
+ respective schemas.
+* _Enum_ - Accepts values that validate against _any_ of the supplied
+ schemas. Similar to a sum type.
+* _List_ - Accepts arrays where each element of the array validates
+ against a supplied schema.
+* _Record_ - Accepts hashes with known keys. Each key has a supplied schema
+ and against which its value must validate.
+* _Value_ - Accepts values using ```==```.
+
+## Usage
+
+Membrane schemas are typically created using a concise DSL. For example, the
+following creates a schema that will validate a hash expecting the key "ints"
+maps to a list of integers and the key "string" maps to a string.
+
+ schema = Membrane::SchemaParser.parse do
+ { "ints" => [Integer],
+ "string" => String,
+ }
+ end
+
+ # Validates successfully
+ schema.validate({
+ "ints" => [1],
+ "string" => "hi",
+ })
+
+ # Fails validation. The key "string" is missing and the value for "ints"
+ # isn't the correct type.
+ schema.validate({
+ "ints" => "invalid",
+ })
+
+This is a more complicated example that illustrate the entire DSL. It should
+be self-explanatory.
+
+ Membrane::SchemaParser.parse do
+ { "ints" => [Integer]
+ "true_or_false" => bool,
+ "anything" => any,
+ optional("_") => any,
+ "one_or_two" => enum(1, 2),
+ "strs_to_ints" => dict(String, Integer),
+ }
+ end
+
+## Adding new schemas
+
+Adding a new schema is trivial. Any class implementing the following "interface"
+can be used as a schema:
+
+ # @param [Object] The object being validated.
+ #
+ # @return [String, nil] On success, nil is returned. On failure, a
+ # description of the validation error is returned.
+ def validate(object)
+
+If you wish to include your new schema as part of the DSL, you'll need to
+modify ```membrane/schema_parser.rb``` and have your class inherit from
+```Membrane::Schema::Base```.
@@ -0,0 +1,15 @@
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
+require "ci/reporter/rake/rspec"
+require "rspec/core/rake_task"
+
+desc "Run all specs"
+RSpec::Core::RakeTask.new("spec") do |t|
+ t.rspec_opts = %w[--color --format documentation]
+end
+
+desc "Run all specs and provide output for ci"
+RSpec::Core::RakeTask.new("spec:ci" => "ci:setup:rspec") do |t|
+ t.rspec_opts = %w[--no-color --format documentation]
+end
+
@@ -0,0 +1,4 @@
+require "membrane/errors"
+require "membrane/schema"
+require "membrane/schema_parser"
+require "membrane/version"
@@ -0,0 +1,3 @@
+module Membrane
+ class SchemaValidationError < StandardError; end
+end
@@ -0,0 +1,15 @@
+require "membrane/schema/any"
+require "membrane/schema/base"
+require "membrane/schema/bool"
+require "membrane/schema/class"
+require "membrane/schema/dictionary"
+require "membrane/schema/enum"
+require "membrane/schema/list"
+require "membrane/schema/record"
+require "membrane/schema/value"
+
+module Membrane
+ module Schema
+ ANY = Membrane::Schema::Any.new
+ end
+end
@@ -0,0 +1,13 @@
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Any < Membrane::Schema::Base
+ def validate(object)
+ nil
+ end
+end
@@ -0,0 +1,17 @@
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Base
+ # Verifies whether or not the supplied object conforms to this schema
+ #
+ # @param [Object] The object being validated
+ #
+ # @raise [Membrane::SchemaValidationError]
+ #
+ # @return [nil]
+ def validate(object)
+ raise NotImplementedError
+ end
+end
@@ -0,0 +1,20 @@
+require "set"
+
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Bool < Membrane::Schema::Base
+ TRUTH_VALUES = Set.new([true, false])
+
+ def validate(object)
+ if !TRUTH_VALUES.include?(object)
+ emsg = "Expected instance of true or false, given #{object}"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+ end
+end
@@ -0,0 +1,24 @@
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Class < Membrane::Schema::Base
+ attr_reader :klass
+
+ def initialize(klass)
+ @klass = klass
+ end
+
+ # Validates whether or not the supplied object is derived from klass
+ def validate(object)
+ if !object.kind_of?(@klass)
+ emsg = "Expected instance of #{@klass}," \
+ + " given an instance of #{object.class}"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+ end
+end
@@ -0,0 +1,40 @@
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Dictionary
+ attr_reader :key_schema
+ attr_reader :value_schema
+
+ def initialize(key_schema, value_schema)
+ @key_schema = key_schema
+ @value_schema = value_schema
+ end
+
+ def validate(object)
+ if !object.kind_of?(Hash)
+ emsg = "Expected instance of Hash, given instance of #{object.class}."
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+
+ errors = {}
+
+ object.each do |k, v|
+ begin
+ @key_schema.validate(k)
+ @value_schema.validate(v)
+ rescue Membrane::SchemaValidationError => e
+ errors[k] = e.to_s
+ end
+ end
+
+ if errors.size > 0
+ emsg = "{ " + errors.map { |k, e| "#{k} => #{e}" }.join(", ") + " }"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+ end
+end
@@ -0,0 +1,30 @@
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Enum < Membrane::Schema::Base
+ attr_reader :elem_schemas
+
+ def initialize(*elem_schemas)
+ @elem_schemas = elem_schemas
+ @elem_schema_str = elem_schemas.map { |s| s.to_s }.join(", ")
+ end
+
+ def validate(object)
+ @elem_schemas.each do |schema|
+ begin
+ schema.validate(object)
+ return nil
+ rescue Membrane::SchemaValidationError
+ end
+ end
+
+ emsg = "Object #{object} doesn't validate" \
+ + " against any of #{@elem_schema_str}"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+end
@@ -0,0 +1,37 @@
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::List < Membrane::Schema::Base
+ attr_reader :elem_schema
+
+ def initialize(elem_schema)
+ @elem_schema = elem_schema
+ end
+
+ def validate(object)
+ if !object.kind_of?(Array)
+ emsg = "Expected instance of Array, given instance of #{object.class}"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+
+ errors = {}
+
+ object.each_with_index do |elem, ii|
+ begin
+ @elem_schema.validate(elem)
+ rescue Membrane::SchemaValidationError => e
+ errors[ii] = e.to_s
+ end
+ end
+
+ if errors.size > 0
+ emsg = errors.map { |ii, e| "At index #{ii}: #{e}" }.join(", ")
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+ end
+end
@@ -0,0 +1,45 @@
+require "set"
+
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Record < Membrane::Schema::Base
+ attr_reader :schemas
+ attr_reader :optional_keys
+
+ def initialize(schemas, optional_keys = [])
+ @optional_keys = Set.new(optional_keys)
+ @schemas = schemas
+ end
+
+ def validate(object)
+ unless object.kind_of?(Hash)
+ emsg = "Expected instance of Hash, given instance of #{object.class}"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+
+ key_errors = {}
+
+ @schemas.each do |k, schema|
+ if object.has_key?(k)
+ begin
+ schema.validate(object[k])
+ rescue Membrane::SchemaValidationError => e
+ key_errors[k] = e.to_s
+ end
+ elsif !@optional_keys.include?(k)
+ key_errors[k] = "Missing key"
+ end
+ end
+
+ if key_errors.size > 0
+ emsg = "{ " + key_errors.map { |k, e| "#{k} => #{e}" }.join(", ") + " }"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+ end
+end
@@ -0,0 +1,22 @@
+require "membrane/errors"
+require "membrane/schema/base"
+
+module Membrane
+ module Schema
+ end
+end
+
+class Membrane::Schema::Value < Membrane::Schema::Base
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def validate(object)
+ if object != @value
+ emsg = "Expected #{@value}, given #{object}"
+ raise Membrane::SchemaValidationError.new(emsg)
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 5536744

Please sign in to comment.