jschematic 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.
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