respect 0.1.0

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