jschematic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.rspec +1 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +40 -0
  5. data/LICENSE +20 -0
  6. data/README.md +48 -0
  7. data/Rakefile +10 -0
  8. data/cucumber.yml +1 -0
  9. data/features/default.feature +72 -0
  10. data/features/dependencies.feature +73 -0
  11. data/features/enum.feature +26 -0
  12. data/features/format.feature +40 -0
  13. data/features/id.feature +75 -0
  14. data/features/items.feature +90 -0
  15. data/features/min_length_max_length.feature +20 -0
  16. data/features/minimum_maximum.feature +29 -0
  17. data/features/pattern.feature +6 -0
  18. data/features/pattern_properties.feature +18 -0
  19. data/features/properties.feature +124 -0
  20. data/features/required.feature +42 -0
  21. data/features/step_definitions/jschematic_steps.rb +62 -0
  22. data/features/support/env.rb +6 -0
  23. data/features/type.feature +76 -0
  24. data/jschematic.gemspec +22 -0
  25. data/lib/jschematic/attributes/additional_items.rb +35 -0
  26. data/lib/jschematic/attributes/additional_properties.rb +28 -0
  27. data/lib/jschematic/attributes/dependencies.rb +26 -0
  28. data/lib/jschematic/attributes/enum.rb +18 -0
  29. data/lib/jschematic/attributes/exclusive_maximum.rb +21 -0
  30. data/lib/jschematic/attributes/exclusive_minimum.rb +21 -0
  31. data/lib/jschematic/attributes/format.rb +56 -0
  32. data/lib/jschematic/attributes/items.rb +24 -0
  33. data/lib/jschematic/attributes/max_items.rb +18 -0
  34. data/lib/jschematic/attributes/max_length.rb +18 -0
  35. data/lib/jschematic/attributes/maximum.rb +22 -0
  36. data/lib/jschematic/attributes/min_items.rb +18 -0
  37. data/lib/jschematic/attributes/min_length.rb +18 -0
  38. data/lib/jschematic/attributes/minimum.rb +22 -0
  39. data/lib/jschematic/attributes/pattern.rb +18 -0
  40. data/lib/jschematic/attributes/pattern_properties.rb +23 -0
  41. data/lib/jschematic/attributes/properties.rb +38 -0
  42. data/lib/jschematic/attributes/required.rb +28 -0
  43. data/lib/jschematic/attributes/type.rb +61 -0
  44. data/lib/jschematic/attributes/unique_items.rb +18 -0
  45. data/lib/jschematic/attributes.rb +28 -0
  46. data/lib/jschematic/element.rb +25 -0
  47. data/lib/jschematic/errors.rb +34 -0
  48. data/lib/jschematic/schema.rb +77 -0
  49. data/lib/jschematic/validation_error.rb +13 -0
  50. data/lib/jschematic.rb +13 -0
  51. data/spec/jschematic/attributes/enum_spec.rb +14 -0
  52. data/spec/jschematic/attributes/max_items_spec.rb +15 -0
  53. data/spec/jschematic/attributes/min_items_spec.rb +15 -0
  54. data/spec/jschematic/attributes/minimum_maximum_spec.rb +33 -0
  55. data/spec/jschematic/attributes/required_spec.rb +29 -0
  56. data/spec/jschematic/attributes/type_spec.rb +63 -0
  57. data/spec/jschematic/attributes_spec.rb +12 -0
  58. data/spec/jschematic/errors_spec.rb +43 -0
  59. data/spec/jschematic/schema_spec.rb +58 -0
  60. data/spec/spec_helper.rb +16 -0
  61. metadata +199 -0
@@ -0,0 +1,22 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class Minimum
6
+ include Jschematic::Element
7
+
8
+ attr_reader :minimum
9
+
10
+ def initialize(minimum)
11
+ @minimum = minimum
12
+ end
13
+
14
+ def accepts?(number)
15
+ return true unless minimum
16
+ return true unless (number.kind_of?(Integer) || number.kind_of?(Float))
17
+
18
+ (number >= minimum) || fail_validation!(">= #{@minimum}", number)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class Pattern
6
+ include Jschematic::Element
7
+
8
+ def initialize(pattern)
9
+ @pattern = Regexp.new(pattern)
10
+ end
11
+
12
+ def accepts?(instance)
13
+ return true unless String === instance
14
+ instance.match(@pattern) || fail_validation!("string matching #{@pattern}", instance)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class PatternProperties
6
+ include Jschematic::Element
7
+
8
+ def initialize(schema)
9
+ @schema = schema
10
+ end
11
+
12
+ def accepts?(instance)
13
+ instance.all? do |property, value|
14
+ if match = @schema.find{ |re, schema| property =~ Regexp.new(re) }
15
+ Schema.new(match[1]).accepts?(value)
16
+ else
17
+ false
18
+ end
19
+ end || fail_validation!(@schema, instance)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class Properties
6
+ include Jschematic::Element
7
+
8
+ def initialize(properties)
9
+ @schemas = properties.inject({}) do |schemas, (name, schema)|
10
+ schemas[name] = Schema.new(schema)
11
+ schemas[name].parent = self
12
+ schemas
13
+ end
14
+ end
15
+
16
+ def accepts?(instance)
17
+ @schemas.all? do |name, schema|
18
+ value = instance.fetch(name) do |missing|
19
+ return true unless schema.required?
20
+ fail_validation!(missing, nil) unless schema.default
21
+ schema.default
22
+ end
23
+ schema.accepts?(value) || fail_validation!(name, value)
24
+ end
25
+ end
26
+
27
+ def each(&block)
28
+ @schemas.values.each do |schema|
29
+ block.call(schema)
30
+ end
31
+ end
32
+
33
+ def id
34
+ @parent.id
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,28 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class Required
6
+ include Jschematic::Element
7
+
8
+ def initialize(required=false)
9
+ case required
10
+ when TrueClass, FalseClass
11
+ @required = required
12
+ else
13
+ raise "Require must be strictly true or false. Truthy and false values are not allowed."
14
+ end
15
+ end
16
+
17
+ def required?
18
+ @required
19
+ end
20
+
21
+ def accepts?(instance)
22
+ if @required
23
+ instance || fail_validation!(@required, instance)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class Type
6
+ include Jschematic::Element
7
+
8
+ attr_reader :type
9
+
10
+ def initialize(type)
11
+ @type = type
12
+ end
13
+
14
+ def accepts?(instance)
15
+ return true unless type
16
+
17
+ case type
18
+ when /^object$/
19
+ assert_kind_of([Hash], instance)
20
+ when /^number$/
21
+ assert_kind_of([Float, Integer], instance)
22
+ when /^integer$/
23
+ assert_kind_of([Integer], instance)
24
+ when /^boolean$/
25
+ assert_kind_of([TrueClass, FalseClass], instance)
26
+ when /^null$/
27
+ assert_kind_of([NilClass], instance)
28
+ when /^any$/
29
+ true
30
+ when Array # union
31
+ # TODO: this is gross. A specific Union type is likely called for.
32
+ type.any? do |union_type|
33
+ begin
34
+ if String===union_type
35
+ Type.new(union_type).accepts?(instance)
36
+ elsif Hash===union_type
37
+ Schema.new(union_type).accepts?(instance)
38
+ end
39
+ rescue ValidationError
40
+ false
41
+ end
42
+ end
43
+ else
44
+ # TODO: probably worth just putting in explicit mapping for all
45
+ # JSON schema types--there are only a few left
46
+ assert_kind_of([constantize(type)], instance)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def assert_kind_of(klassen, instance)
53
+ klassen.any?{ |klass| instance.kind_of?(klass) } || fail_validation!(klassen, instance)
54
+ end
55
+
56
+ def constantize(string)
57
+ Kernel.const_get(string.capitalize)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,18 @@
1
+ require 'jschematic/element'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ class UniqueItems
6
+ include Jschematic::Element
7
+
8
+ def initialize(value=false)
9
+ @value = value
10
+ end
11
+
12
+ def accepts?(instance)
13
+ require true unless Array === instance
14
+ (instance == instance.uniq) || fail_validation!("all items to be unique", instance)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ require 'jschematic/attributes/type'
2
+ require 'jschematic/attributes/properties'
3
+ require 'jschematic/attributes/pattern_properties'
4
+ require 'jschematic/attributes/additional_properties'
5
+ require 'jschematic/attributes/items'
6
+ require 'jschematic/attributes/additional_items'
7
+ require 'jschematic/attributes/required'
8
+ require 'jschematic/attributes/dependencies'
9
+ require 'jschematic/attributes/minimum'
10
+ require 'jschematic/attributes/maximum'
11
+ require 'jschematic/attributes/exclusive_minimum'
12
+ require 'jschematic/attributes/exclusive_maximum'
13
+ require 'jschematic/attributes/min_items'
14
+ require 'jschematic/attributes/max_items'
15
+ require 'jschematic/attributes/unique_items'
16
+ require 'jschematic/attributes/pattern'
17
+ require 'jschematic/attributes/min_length'
18
+ require 'jschematic/attributes/max_length'
19
+ require 'jschematic/attributes/enum'
20
+ require 'jschematic/attributes/format'
21
+
22
+ module Jschematic
23
+ module Attributes
24
+ def self.[](name)
25
+ const_get(name[0].chr.capitalize + name[1..-1])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ require 'jschematic/validation_error'
2
+
3
+ module Jschematic
4
+ module Element
5
+ attr_accessor :parent
6
+
7
+ def required?
8
+ false
9
+ end
10
+
11
+ def title
12
+ nil
13
+ end
14
+
15
+ def to_s
16
+ self.class.to_s
17
+ end
18
+
19
+ private
20
+
21
+ def fail_validation!(expected, actual)
22
+ raise Jschematic::ValidationError.new(self, expected, actual)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ module Jschematic
2
+ class Errors
3
+ include Enumerable
4
+
5
+ def initialize(raise_on_error = false)
6
+ @raise_on_error = raise_on_error
7
+ @errors = []
8
+ end
9
+
10
+ def add(id, desc)
11
+ if @raise_on_error
12
+ raise "Error: #{desc}"
13
+ else
14
+ @errors << [id, desc]
15
+ end
16
+ end
17
+
18
+ def all
19
+ @errors
20
+ end
21
+
22
+ def empty?
23
+ @errors.empty?
24
+ end
25
+
26
+ def reset!
27
+ @errors = []
28
+ end
29
+
30
+ def each(&block)
31
+ @errors.each(&block)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,77 @@
1
+ require 'addressable/uri'
2
+
3
+ require 'jschematic/errors'
4
+ require 'jschematic/element'
5
+ require 'jschematic/attributes'
6
+
7
+ module Jschematic
8
+ class Schema
9
+ include Enumerable
10
+ include Jschematic::Element
11
+
12
+ attr_reader :default, :title, :description, :id
13
+ attr_writer :parent
14
+
15
+ def initialize(raw_schema)
16
+ @raw_schema = raw_schema.dup || {}
17
+
18
+ @default = @raw_schema.delete("default")
19
+ @title = @raw_schema.delete("title") || ""
20
+ @description = @raw_schema.delete("description") || ""
21
+ @id = Addressable::URI.parse(@raw_schema.delete("id") || "")
22
+
23
+ @attributes = []
24
+
25
+ @raw_schema.each_pair do |attribute, value|
26
+ begin
27
+ attribute = Attributes[attribute].new(value){ |dep| @raw_schema[dep] }
28
+ attribute.parent = self
29
+ @attributes << attribute
30
+ rescue NameError => e
31
+ # Not finding an attribute is not necessarily an error, but this is
32
+ # obviously not the right way to handle it. Need to find a better way to
33
+ # report information.
34
+ # should we create accessors for property on the schema?
35
+ # we could have Attributes.[] raise a special exception rather than NameError
36
+ puts "NameError #{e} encountered... continuing"
37
+ end
38
+ end
39
+ end
40
+
41
+ def accepts?(instance)
42
+ @attributes.all?{ |child| child.accepts?(add_default(instance)) }
43
+ end
44
+
45
+ def required?
46
+ @attributes.any?{ |child| child.required? }
47
+ end
48
+
49
+ def each(&block)
50
+ block.call(self)
51
+ @attributes.each{ |child| child.each(&block) }
52
+ end
53
+
54
+ def id
55
+ if @parent
56
+ @parent.id + @id
57
+ else
58
+ @id
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def add_default(instance)
65
+ return instance unless default
66
+
67
+ case instance
68
+ when Hash
69
+ @default.keys.each do |key|
70
+ instance[key] = @default[key] unless instance.has_key?(key)
71
+ end
72
+ end
73
+
74
+ instance
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,13 @@
1
+ module Jschematic
2
+ class ValidationError < StandardError
3
+ attr_reader :what, :expected, :actual
4
+
5
+ def initialize(what, expected, actual)
6
+ @what, @expected, @actual = what, expected, actual
7
+ end
8
+
9
+ def to_s
10
+ "#{what} expected #{expected} but found #{actual}"
11
+ end
12
+ end
13
+ end
data/lib/jschematic.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'jschematic/schema'
2
+
3
+ module Jschematic
4
+ def self.validate(instance, schema)
5
+ validate!(instance, schema)
6
+ rescue ValidationError
7
+ false
8
+ end
9
+
10
+ def self.validate!(instance, schema)
11
+ Schema.new(schema).accepts?(instance)
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ describe Enum do
6
+ subject { Enum }
7
+
8
+ it "raises unless its schema value is an Array" do
9
+ expect { subject.new("this is allowed, right?") }.to raise_error
10
+ expect { subject.new(["not", "really"]) }.to_not raise_error
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ describe MaxItems, "with a maximum of two items" do
6
+ subject { MaxItems.new(2) }
7
+
8
+ describe "#accepts?" do
9
+ it { should accept([1,2]) }
10
+ it { should_not accept([1,2,3]) }
11
+ it { should accept("instance that is not an array") }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ describe MinItems, "with a minimum of two items" do
6
+ subject { MinItems.new(2) }
7
+
8
+ describe "#accepts?" do
9
+ it { should accept([1,2]) }
10
+ it { should_not accept([1]) }
11
+ it { should accept("instance that is not an array") }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ describe Minimum do
6
+ subject { Minimum.new(2112) }
7
+
8
+ it { should accept("portokalli") }
9
+ end
10
+
11
+ describe Maximum do
12
+ subject { Maximum.new(2112) }
13
+
14
+ it { should accept("portokalli") }
15
+ end
16
+
17
+ describe ExclusiveMinimum do
18
+ subject { ExclusiveMinimum }
19
+
20
+ it "raises if minimum is not defined" do
21
+ expect { subject.new(true){|needed| nil} }.to raise_error(/'exclusiveMinimum' depends on 'minimum'/)
22
+ end
23
+ end
24
+
25
+ describe ExclusiveMaximum do
26
+ subject { ExclusiveMaximum }
27
+
28
+ it "raises if maximum is not defined" do
29
+ expect { subject.new(true){|needed| nil} }.to raise_error(/'exclusiveMaximum' depends on 'maximum'/)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ describe Required do
6
+ describe "#required?" do
7
+ subject { Required }
8
+
9
+ context "by default" do
10
+ it "answers false" do
11
+ subject.new.should_not be_required
12
+ end
13
+ end
14
+
15
+ context "when true" do
16
+ it "answers true" do
17
+ subject.new(true).should be_required
18
+ end
19
+ end
20
+
21
+ context "when truthy or falsy" do
22
+ it "raises an error" do
23
+ expect { subject.new("true") }.to raise_error
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ module Attributes
5
+ describe Type do
6
+ context "string" do
7
+ subject { Type.new("string") }
8
+ it { should_not accept(nil) }
9
+ end
10
+
11
+ context "number" do
12
+ subject { Type.new("number") }
13
+ it { should_not accept(nil) }
14
+ it { should accept(2112) }
15
+ end
16
+
17
+ context "integer" do
18
+ subject { Type.new("integer") }
19
+ it { should_not accept(nil) }
20
+ end
21
+
22
+ context "boolean" do
23
+ subject { Type.new("boolean") }
24
+ it { should_not accept(nil) }
25
+ it { should_not accept("true") }
26
+ it { should_not accept([1,2,3]) }
27
+ end
28
+
29
+ context "object" do
30
+ subject { Type.new("object") }
31
+ it { should_not accept(nil) }
32
+ it { should_not accept([1,2,3]) }
33
+ it { should_not accept("string") }
34
+ end
35
+
36
+ context "array" do
37
+ subject { Type.new("array") }
38
+ it { should_not accept(nil) }
39
+ it { should_not accept({:foo => "bar"}) }
40
+ it { should_not accept(1234) }
41
+ end
42
+
43
+ context "null" do
44
+ subject { Type.new("null") }
45
+ it { should accept(nil) }
46
+ it { should_not accept(true) }
47
+ it { should_not accept("string") }
48
+ it { should_not accept([1,2,3]) }
49
+ end
50
+
51
+ context "any" do
52
+ subject { Type.new("any") }
53
+ it { should accept(nil) }
54
+ end
55
+
56
+ context "union" do
57
+ subject { Type.new(["string", "integer"]) }
58
+ it { should_not accept(nil) }
59
+ end
60
+ end
61
+ end
62
+ end
63
+
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ describe Attributes do
5
+ describe ".[]" do
6
+ it "maps from javascript style names to classes" do
7
+ Attributes::ExclusiveMaximum.should_receive(:new).with(1, 2)
8
+ subject["exclusiveMaximum"].new(1, 2)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ module Jschematic
4
+ describe Errors do
5
+ let(:errors) { Errors.new }
6
+
7
+ it "is enumerable" do
8
+ errors.class.should include(Enumerable)
9
+
10
+ errors.add(:ident1, "desc1")
11
+ errors.add(:ident2, "desc2")
12
+
13
+ col = []
14
+ errors.each{ |error| col << error }
15
+
16
+ col.should == [[:ident1, "desc1"], [:ident2, "desc2"]]
17
+ end
18
+
19
+ describe "#add" do
20
+ it "records an error" do
21
+ errors.add(:ident, "description")
22
+ errors.all.should == [[:ident, "description"]]
23
+ end
24
+
25
+ context "when raise on error is true" do
26
+ let(:errors) { Errors.new(true) }
27
+
28
+ it "raises when an error is added" do
29
+ expect { errors.add(:ident, "description") }.to raise_error(/Error: description/)
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "#reset!" do
35
+ it "clears the collected errors" do
36
+ errors.add(:ident, "desc")
37
+ errors.should_not be_empty
38
+ errors.reset!
39
+ errors.should be_empty
40
+ end
41
+ end
42
+ end
43
+ end