respect 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +289 -0
  3. data/RELATED_WORK.md +40 -0
  4. data/RELEASE_NOTES.md +23 -0
  5. data/Rakefile +31 -0
  6. data/STATUS_MATRIX.html +137 -0
  7. data/lib/respect.rb +231 -0
  8. data/lib/respect/any_schema.rb +22 -0
  9. data/lib/respect/array_def.rb +28 -0
  10. data/lib/respect/array_schema.rb +203 -0
  11. data/lib/respect/boolean_schema.rb +32 -0
  12. data/lib/respect/composite_schema.rb +86 -0
  13. data/lib/respect/core_statements.rb +206 -0
  14. data/lib/respect/datetime_schema.rb +27 -0
  15. data/lib/respect/def_without_name.rb +6 -0
  16. data/lib/respect/divisible_by_validator.rb +20 -0
  17. data/lib/respect/doc_helper.rb +24 -0
  18. data/lib/respect/doc_parser.rb +37 -0
  19. data/lib/respect/dsl_dumper.rb +181 -0
  20. data/lib/respect/equal_to_validator.rb +20 -0
  21. data/lib/respect/fake_name_proxy.rb +116 -0
  22. data/lib/respect/float_schema.rb +27 -0
  23. data/lib/respect/format_validator.rb +136 -0
  24. data/lib/respect/global_def.rb +79 -0
  25. data/lib/respect/greater_than_or_equal_to_validator.rb +19 -0
  26. data/lib/respect/greater_than_validator.rb +19 -0
  27. data/lib/respect/has_constraints.rb +34 -0
  28. data/lib/respect/hash_def.rb +40 -0
  29. data/lib/respect/hash_schema.rb +218 -0
  30. data/lib/respect/in_validator.rb +19 -0
  31. data/lib/respect/integer_schema.rb +27 -0
  32. data/lib/respect/ip_addr_schema.rb +23 -0
  33. data/lib/respect/ipv4_addr_schema.rb +27 -0
  34. data/lib/respect/ipv6_addr_schema.rb +27 -0
  35. data/lib/respect/items_def.rb +21 -0
  36. data/lib/respect/json_schema_html_formatter.rb +143 -0
  37. data/lib/respect/less_than_or_equal_to_validator.rb +19 -0
  38. data/lib/respect/less_than_validator.rb +19 -0
  39. data/lib/respect/match_validator.rb +19 -0
  40. data/lib/respect/max_length_validator.rb +20 -0
  41. data/lib/respect/min_length_validator.rb +20 -0
  42. data/lib/respect/multiple_of_validator.rb +10 -0
  43. data/lib/respect/null_schema.rb +26 -0
  44. data/lib/respect/numeric_schema.rb +33 -0
  45. data/lib/respect/org3_dumper.rb +213 -0
  46. data/lib/respect/regexp_schema.rb +19 -0
  47. data/lib/respect/schema.rb +285 -0
  48. data/lib/respect/schema_def.rb +16 -0
  49. data/lib/respect/string_schema.rb +21 -0
  50. data/lib/respect/unit_test_helper.rb +37 -0
  51. data/lib/respect/uri_schema.rb +23 -0
  52. data/lib/respect/utc_time_schema.rb +17 -0
  53. data/lib/respect/validator.rb +51 -0
  54. data/lib/respect/version.rb +3 -0
  55. data/test/any_schema_test.rb +79 -0
  56. data/test/array_def_test.rb +113 -0
  57. data/test/array_schema_test.rb +487 -0
  58. data/test/boolean_schema_test.rb +89 -0
  59. data/test/composite_schema_test.rb +30 -0
  60. data/test/datetime_schema_test.rb +83 -0
  61. data/test/doc_helper_test.rb +34 -0
  62. data/test/doc_parser_test.rb +109 -0
  63. data/test/dsl_dumper_test.rb +395 -0
  64. data/test/fake_name_proxy_test.rb +138 -0
  65. data/test/float_schema_test.rb +146 -0
  66. data/test/format_validator_test.rb +224 -0
  67. data/test/hash_def_test.rb +126 -0
  68. data/test/hash_schema_test.rb +613 -0
  69. data/test/integer_schema_test.rb +142 -0
  70. data/test/ip_addr_schema_test.rb +78 -0
  71. data/test/ipv4_addr_schema_test.rb +71 -0
  72. data/test/ipv6_addr_schema_test.rb +71 -0
  73. data/test/json_schema_html_formatter_test.rb +214 -0
  74. data/test/null_schema_test.rb +46 -0
  75. data/test/numeric_schema_test.rb +294 -0
  76. data/test/org3_dumper_test.rb +784 -0
  77. data/test/regexp_schema_test.rb +54 -0
  78. data/test/respect_test.rb +108 -0
  79. data/test/schema_def_test.rb +405 -0
  80. data/test/schema_test.rb +290 -0
  81. data/test/string_schema_test.rb +209 -0
  82. data/test/support/circle.rb +11 -0
  83. data/test/support/color.rb +24 -0
  84. data/test/support/point.rb +11 -0
  85. data/test/support/respect/circle_schema.rb +16 -0
  86. data/test/support/respect/color_def.rb +19 -0
  87. data/test/support/respect/color_schema.rb +33 -0
  88. data/test/support/respect/point_schema.rb +19 -0
  89. data/test/support/respect/rgba_schema.rb +20 -0
  90. data/test/support/respect/universal_validator.rb +25 -0
  91. data/test/support/respect/user_macros.rb +12 -0
  92. data/test/support/rgba.rb +11 -0
  93. data/test/test_helper.rb +90 -0
  94. data/test/uri_schema_test.rb +54 -0
  95. data/test/utc_time_schema_test.rb +63 -0
  96. data/test/validator_test.rb +22 -0
  97. metadata +288 -0
@@ -0,0 +1,32 @@
1
+ module Respect
2
+ class BooleanSchema < Schema
3
+ include HasConstraints
4
+
5
+ public_class_method :new
6
+
7
+ def validate_type(object)
8
+ case object
9
+ when String
10
+ if object == "true"
11
+ true
12
+ elsif object == "false"
13
+ false
14
+ else
15
+ raise ValidationError,
16
+ "malformed boolean value: `#{object}'"
17
+ end
18
+ when TrueClass, FalseClass
19
+ object
20
+ when NilClass
21
+ if allow_nil?
22
+ nil
23
+ else
24
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
25
+ end
26
+ else
27
+ raise ValidationError, "object is not a boolean but a '#{object.class}'"
28
+ end
29
+ end
30
+
31
+ end # class BooleanSchema
32
+ end # module Respect
@@ -0,0 +1,86 @@
1
+ module Respect
2
+ # A composite schema is a schema composed of another schema.
3
+ #
4
+ # Sub-classing {CompositeSchema} is the easiest way to add a user-defined
5
+ # schema. Indeed, you just have to overwrite {#schema_definition} and optionally
6
+ # {#sanitize}. Your schema will be handled properly by all other part
7
+ # of the library (i.e. mainly dumpers and the DSL).
8
+ #
9
+ # Example:
10
+ # module Respect
11
+ # class PointSchema < CompositeSchema
12
+ # def schema_defintion
13
+ # HashSchema.define do |s|
14
+ # s.numeric "x"
15
+ # s.numeric "y"
16
+ # end
17
+ # end
18
+ #
19
+ # def sanitize(object)
20
+ # # Assuming you have defined a Point class.
21
+ # Point.new(object[:x], object[:y])
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # A "point" method will be available in the DSL so you could use
27
+ # your schema like that:
28
+ #
29
+ # Example:
30
+ # HashSchema.define do |s|
31
+ # s.point "origin"
32
+ # end
33
+ class CompositeSchema < Schema
34
+
35
+ class << self
36
+ def inherited(subclass)
37
+ subclass.public_class_method :new
38
+ end
39
+ end
40
+
41
+ def initialize(options = {})
42
+ super
43
+ @schema = self.schema_definition
44
+ end
45
+
46
+ # Returns the schema composing this schema.
47
+ attr_reader :schema
48
+
49
+ # Overloaded methods (see {Schema#validate}).
50
+ def validate(object)
51
+ # Handle nil case.
52
+ if object.nil?
53
+ if allow_nil?
54
+ self.sanitized_object = nil
55
+ return true
56
+ else
57
+ raise ValidationError, "object is nil but this #{self.class.name} does not allow nil"
58
+ end
59
+ end
60
+ @schema.validate(object)
61
+ self.sanitized_object = sanitize(@schema.sanitized_object)
62
+ true
63
+ rescue ValidationError => e
64
+ # Reset sanitized object.
65
+ self.sanitized_object = nil
66
+ raise e
67
+ end
68
+
69
+ # Returns the schema composing this composite schema.
70
+ # Overwrite this methods in sub-class.
71
+ def schema_definition
72
+ raise NoMethodError, "implement me in sub-class"
73
+ end
74
+
75
+ # Sanitize the given validated +object+. Overwrite this method
76
+ # in sub-class and returns the object that would be inserted
77
+ # in the sanitized object. The object passed as argument
78
+ # is an already sanitized sub-part of the overall object
79
+ # being validated. By default this method is a no-op. It
80
+ # returns the given +object+.
81
+ def sanitize(object)
82
+ object
83
+ end
84
+
85
+ end # class CompositeSchema
86
+ end # module Respect
@@ -0,0 +1,206 @@
1
+ module Respect
2
+ # Core DSL statements definition module.
3
+ #
4
+ # This module holds all the basic statements available in the DSL. It is included
5
+ # in all the DSL context using {Respect.extend_dsl_with}. Thus all basic DSL
6
+ # contexts provides its feature.
7
+ #
8
+ # Most of the statements are available as dynamic methods.
9
+ # For each "FooSchema" class defined in the {Respect} module (and following certain
10
+ # condition described at {Respect.schema_for}), there is a statement
11
+ # "foo" (see {Schema.statement_name}) expecting a name, some options and a block.
12
+ # This statement defines a new "FooSchema" with the given options and block. This
13
+ # schema is stored in the current context using the given name. The name may be used
14
+ # differently depending on the context. In a hash definition context it will be
15
+ # used as a property name whereas it will be simply ignored in the
16
+ # context of an array. Context classes including the {DefWithoutName} module ignore
17
+ # the name argument whereas others do not. The {FakeNameProxy} is in charge of
18
+ # transparently passing +nil+ for the name in contexts including the {DefWithoutName}
19
+ # module.
20
+ #
21
+ # Example:
22
+ # HashSchema.define do |s|
23
+ # # method_missing calls:
24
+ # # update_context("i", IntegerSchema.define({greater_than: 42}))
25
+ # s.integer "i", greater_than: 42
26
+ # end
27
+ # ArraySchema.define do |s|
28
+ # # method_missing calls:
29
+ # # update_context(nil, IntegerSchema.define({greater_than: 42}))
30
+ # s.integer greater_than: 42
31
+ # end
32
+ #
33
+ # Classes including this module must implement the +update_context+ method
34
+ # which is supposed to update the schema under definition with the given
35
+ # name and schema created by the method.
36
+ #
37
+ # Do not include your helper module in this module since definition classes
38
+ # including it won't be affected due to the
39
+ # {dynamic module include problem}[http://eigenclass.org/hiki/The+double+inclusion+problem].
40
+ # To extend the DSL use {Respect.extend_dsl_with} instead.
41
+ #
42
+ # It is recommended that your macros implementation be based on core statements
43
+ # because +update_context+ API is *experimental*. If you do so anyway your
44
+ # macros may not work properly with the {#doc} and {#with_options} statements.
45
+ module CoreStatements
46
+
47
+ # @!method string(name, options = {})
48
+ # Define a {StringSchema} with the given +options+ and stores it in the
49
+ # current context using +name+ as index.
50
+ # @!method integer(name, options = {})
51
+ # Define a {IntegerSchema} with the given +options+ and stores it in the
52
+ # current context using +name+ as index.
53
+ # @!method float(name, options = {})
54
+ # Define a {FloatSchema} with the given +options+ and stores it in the
55
+ # current context using +name+ as index.
56
+ # @!method numeric(name, options = {})
57
+ # Define a {NumericSchema} with the given +options+ and stores it in the
58
+ # current context using +name+ as index.
59
+ # @!method any(name, options = {})
60
+ # Define a {AnySchema} with the given +options+ and stores it in the
61
+ # current context using +name+ as index.
62
+ # @!method null(name, options = {})
63
+ # Define a {NullSchema} with the given +options+ and stores it in the
64
+ # current context using +name+ as index.
65
+ # @!method boolean(name, options = {})
66
+ # Define a {BooleanSchema} with the given +options+ and stores it in the
67
+ # current context using +name+ as index.
68
+ # @!method uri(name, options = {})
69
+ # Define a {URISchema} with the given +options+ and stores it in the
70
+ # current context using +name+ as index.
71
+ # @!method hash(name, options = {}, &block)
72
+ # Define a {HashSchema} with the given +options+ and +block+ stores it
73
+ # in the current context using +name+ as index.
74
+ # @!method array(name, options = {})
75
+ # Define a {ArraySchema} with the given +options+ and +block+ stores it
76
+ # in the current context using +name+ as index.
77
+ # @!method datetime(name, options = {})
78
+ # Define a {DatetimeSchema} with the given +options+ and stores it in the
79
+ # current context using +name+ as index.
80
+ # @!method ip_addr(name, options = {})
81
+ # Define a {IPAddrSchema} with the given +options+ and stores it in the
82
+ # current context using +name+ as index.
83
+ # @!method ipv4_addr(name, options = {})
84
+ # Define a {Ipv4AddrSchema} with the given +options+ and stores it in the
85
+ # current context using +name+ as index.
86
+ # @!method ipv6_addr(name, options = {})
87
+ # Define a {Ipv6AddrSchema} with the given +options+ and stores it in the
88
+ # current context using +name+ as index.
89
+ # @!method regexp(name, options = {})
90
+ # Define a {RegexpSchema} with the given +options+ and stores it in the
91
+ # current context using +name+ as index.
92
+ # @!method utc_time(name, options = {})
93
+ # Define a {UTCTimeSchema} with the given +options+ and stores it in the
94
+ # current context using +name+ as index.
95
+ #
96
+ # Call +update_context+ using the first argument as index and passes the rest
97
+ # to the {Schema.define} class method of the schema class associated with the method name.
98
+ # As a consequence any call to missing method +foo+ will define a +FooSchema+
99
+ # schema using +FooSchema.define+.
100
+ #
101
+ # The options are merged with the default options which may include the +:doc+
102
+ # option if {#doc} has been called before. The current documentation is reset
103
+ # after this call.
104
+ #
105
+ # Note that if you define a new schema named after a method already defined in
106
+ # a context class such as {GlobalDef} or its sub-classes or in +Object+, the
107
+ # dynamic dispatch won't work. For instance even if you have defined the
108
+ # +ClassSchema+ class the following code won't work as expected:
109
+ #
110
+ # Schema.define do |s|
111
+ # s.class # Call Object#class !!!!!
112
+ # end
113
+ #
114
+ # To prevent this problem you must undefine the method in the DSL by doing
115
+ # something like that:
116
+ #
117
+ # module Respect
118
+ # class GlobalDef
119
+ # undef_method :class
120
+ # end
121
+ # end
122
+ #
123
+ # or you can overwrite the +class+ method in the context of your choice:
124
+ #
125
+ # module Respect
126
+ # class GlobalDef
127
+ # def class(name, options = {}, &block)
128
+ # update_context name, ClassSchema.define(options, &block)
129
+ # end
130
+ # end
131
+ # end
132
+ #
133
+ # Do not un-define or overwrite 'method' and 'methods' since {FakeNameProxy}
134
+ # use them.
135
+ def method_missing(method_name, *args, &block)
136
+ if respond_to_missing?(method_name, false)
137
+ size_range = 1..2
138
+ if size_range.include? args.size
139
+ name = args.shift
140
+ default_options = {}
141
+ default_options.merge!(@options) unless @options.nil?
142
+ default_options[:doc] = @doc unless @doc.nil?
143
+ if options = args.shift
144
+ options = default_options.merge(options)
145
+ else
146
+ options = default_options
147
+ end
148
+ @doc = nil
149
+ update_context name, Respect.schema_for(method_name).define(options, &block)
150
+ else
151
+ expected_size = args.size > size_range.end ? size_range.end : size_range.begin
152
+ raise ArgumentError, "wrong number of argument (#{args.size} for #{expected_size})"
153
+ end
154
+ else
155
+ super
156
+ end
157
+ end
158
+
159
+ def respond_to_missing?(method_name, include_all)
160
+ Respect.schema_defined_for?(method_name)
161
+ end
162
+
163
+ # @!method email(name, options = {})
164
+ # Define a string formatted an email (see {FormatValidator#validate_email}).
165
+ # @!method phone_number(name, options = {})
166
+ # Define a string formatted as a phone number (see {FormatValidator#validate_phone_number}).
167
+ # @!method hostname(name, options = {})
168
+ # Define a string formatted as a machine host name (see {FormatValidator#validate_hostname}).
169
+ [
170
+ :email,
171
+ :phone_number,
172
+ :hostname,
173
+ ].each do |meth_name|
174
+ define_method(meth_name) do |name, options = {}|
175
+ string name, options.dup.merge(format: meth_name)
176
+ end
177
+ end
178
+
179
+ # Define the current documentation text. It will be used as documentation for the
180
+ # next defined schema. It can be used once, so it is reset once it has been affected
181
+ # to a schema.
182
+ #
183
+ # Example:
184
+ # s = HashSchema.define do |s|
185
+ # s.doc "A magic number"
186
+ # s.integer "magic"
187
+ # s.integer "nodoc"
188
+ # s.doc "A parameter..."
189
+ # s.string "param"
190
+ # end
191
+ # s["magic"].doc #=> "A magic number"
192
+ # s["nodoc"].doc #=> nil
193
+ # s["param"].doc #=> "A parameter..."
194
+ def doc(text)
195
+ @doc = text
196
+ end
197
+
198
+ # Use +options+ as the default for all schema created within +block+.
199
+ def with_options(options, &block)
200
+ @options = options
201
+ FakeNameProxy.new(self).eval(&block)
202
+ @options = nil
203
+ end
204
+
205
+ end # module CoreStatements
206
+ end # module Respect
@@ -0,0 +1,27 @@
1
+ module Respect
2
+ # Validate a date and time string following RFC 3399.
3
+ # +DateTime+, +Time+, and +Date+ object are accepted.
4
+ # If validation succeed the sanitized object is a +DateTime+ object.
5
+ class DatetimeSchema < StringSchema
6
+
7
+ def validate_type(object)
8
+ case object
9
+ when NilClass
10
+ if allow_nil?
11
+ nil
12
+ else
13
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
14
+ end
15
+ when DateTime
16
+ object
17
+ when Time
18
+ object.to_datetime
19
+ when Date
20
+ object.to_date
21
+ else
22
+ FormatValidator.new(:datetime).validate(object)
23
+ end
24
+ end
25
+
26
+ end # class DatetimeSchema
27
+ end # module Respect
@@ -0,0 +1,6 @@
1
+ module Respect
2
+ # Include this module in DSL classes defining methods
3
+ # not accepting name as first argument.
4
+ module DefWithoutName
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ module Respect
2
+ class DivisibleByValidator < Validator
3
+ def initialize(divider)
4
+ @divider = divider
5
+ end
6
+
7
+ def validate(value)
8
+ unless (value % @divider).zero?
9
+ raise ValidationError, "#{value} is not divisible by #@divider"
10
+ end
11
+ end
12
+
13
+ private
14
+
15
+ def to_h_org3
16
+ { 'divisibleBy' => @divider }
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ module Respect
2
+ # Convenient module to ease usage of {DocParser}.
3
+ #
4
+ # Include it in classes returning their documentation via a +documentation+ method.
5
+ # This module provides a {#title} and {#description} methods for extracting
6
+ # them from the documentation.
7
+ module DocHelper
8
+ # Returns the title part of the documentation returned by +documentation+ method
9
+ # (+nil+ if it does not have any).
10
+ def title
11
+ if documentation
12
+ DocParser.new.parse(documentation.to_s).title
13
+ end
14
+ end
15
+
16
+ # Returns the description part of the documentation returned by +documentation+ method
17
+ # (+nil+ if it does not have any).
18
+ def description
19
+ if documentation
20
+ DocParser.new.parse(documentation.to_s).description
21
+ end
22
+ end
23
+ end
24
+ end # module Respect
@@ -0,0 +1,37 @@
1
+ module Respect
2
+ class DocParser
3
+
4
+ def initialize
5
+ @title = nil
6
+ @description = nil
7
+ end
8
+
9
+ def parse(string)
10
+ ss = StringScanner.new(string)
11
+ if ss.scan_until(/\n/)
12
+ if ss.eos?
13
+ @title = ss.pre_match
14
+ else
15
+ if ss.scan(/\n+/)
16
+ @title = ss.pre_match.chop
17
+ unless ss.rest.empty?
18
+ @description = ss.rest
19
+ end
20
+ else
21
+ if ss.eos?
22
+ @title = string.chop
23
+ else
24
+ @description = string
25
+ end
26
+ end
27
+ end
28
+ else
29
+ @title = string
30
+ end
31
+ self
32
+ end
33
+
34
+ attr_reader :title, :description
35
+
36
+ end # class DocParser
37
+ end # module Respect