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,181 @@
1
+ module Respect
2
+ # Dump a schema to a string representation using the DSL syntax so you
3
+ # can evaluate it and get the same schema back.
4
+ #
5
+ # Theoretically, this must always be true:
6
+ # eval(DslDumper.new(schema).dump) == schema
7
+ #
8
+ # The current implementation covers all the _Schema_ and _Validator_
9
+ # classes defined in this package. User-defined sub-class of {Schema}
10
+ # are not guarantee to work. Specially those using a custom "Def" class
11
+ # or with special attributes. The API of this dumper is *experimental*,
12
+ # so relying on it to teach the dumper how to dump a user-defined schema
13
+ # class may break in future releases.
14
+ #
15
+ # However, sub-classes of {CompositeSchema} are handled
16
+ # properly as well as all {Validator} sub-classes.
17
+ class DslDumper
18
+
19
+ def initialize(schema)
20
+ @schema = schema
21
+ @indent_level = 0
22
+ @indent_size = 2
23
+ @context_data = {}
24
+ end
25
+
26
+ attr_reader :schema, :output
27
+
28
+ def dump(output = nil)
29
+ @output = output
30
+ @output ||= String.new
31
+ self << "Respect::Schema.define do |s|"
32
+ self.indent do
33
+ self.dump_schema(@schema)
34
+ end
35
+ self << "\nend\n"
36
+ @output
37
+ end
38
+
39
+ def <<(str)
40
+ @output << str.gsub(/(\n+)/, "\\1#{indentation}")
41
+ self
42
+ end
43
+
44
+ def indent(count = 1, &block)
45
+ @indent_level += count
46
+ if block
47
+ block.call
48
+ unindent(count)
49
+ end
50
+ end
51
+
52
+ def unindent(count = 1)
53
+ @indent_level -= count
54
+ end
55
+
56
+ def indentation
57
+ " " * @indent_size * @indent_level
58
+ end
59
+
60
+ attr_accessor :context_data
61
+
62
+ def dump_block(args = [ "s" ], &block)
63
+ self << " do |#{args.join(', ')}|"
64
+ self.indent(&block)
65
+ self << "\nend"
66
+ end
67
+
68
+ def dump_schema(schema)
69
+ self.dump_doc(schema)
70
+ self << "\ns."
71
+ self.dump_name(schema)
72
+ self.dump_arguments(schema)
73
+ self.dump_body(schema)
74
+ self
75
+ end
76
+
77
+ def dump_doc(schema, prefix = false)
78
+ if schema.doc
79
+ if schema.description
80
+ self << "\n"
81
+ self << %q{s.doc <<-EOS.strip_heredoc}
82
+ self.indent do
83
+ self << "\n"
84
+ self << schema.doc
85
+ self << "EOS"
86
+ end
87
+ else
88
+ self << "\ns.doc \"#{schema.title}\""
89
+ end
90
+ end
91
+ end
92
+
93
+ def dump_name(schema)
94
+ self << schema.class.statement_name
95
+ end
96
+
97
+ def dump_arguments(schema)
98
+ # Fetch name if there is one?
99
+ if self.context_data.has_key?(:name)
100
+ name = self.context_data[:name]
101
+ else
102
+ name = nil
103
+ end
104
+ # Compute options to dump.
105
+ options = schema.non_default_options.reject do |opt|
106
+ opt == :doc
107
+ end
108
+ # Dump name and options
109
+ if name || !options.empty?
110
+ if name
111
+ self << " "
112
+ self << name.inspect
113
+ end
114
+ if !options.empty?
115
+ if name
116
+ self << ","
117
+ end
118
+ self << " "
119
+ self.dump_options(schema)
120
+ end
121
+ end
122
+ self
123
+ end
124
+
125
+ def dump_options(schema)
126
+ options = schema.non_default_options
127
+ option_keys = options.keys
128
+ option_keys.each do |opt|
129
+ self << opt.inspect
130
+ self << " => "
131
+ self << options[opt].inspect
132
+ self << ", " unless opt == option_keys.last
133
+ end
134
+ self
135
+ end
136
+
137
+ def dump_body(schema)
138
+ symbol = "dump_body_for_#{schema.class.statement_name}"
139
+ if respond_to? symbol
140
+ send(symbol, schema)
141
+ end
142
+ end
143
+
144
+ def dump_body_for_hash(schema)
145
+ dump_block do
146
+ schema.properties.each do |name, schema|
147
+ context_data[:name] = name
148
+ dump_schema(schema)
149
+ end
150
+ end
151
+ end
152
+
153
+ def dump_body_for_array(schema)
154
+ dump_block do
155
+ context_data.delete(:name)
156
+ if schema.item
157
+ dump_schema(schema.item)
158
+ end
159
+ if schema.items && !schema.items.empty?
160
+ self << "\ns.items do |s|"
161
+ indent do
162
+ schema.items.each do |schema|
163
+ dump_schema(schema)
164
+ end
165
+ end
166
+ self << "\nend"
167
+ end
168
+ if schema.extra_items && !schema.extra_items.empty?
169
+ self << "\ns.extra_items do |s|"
170
+ indent do
171
+ schema.extra_items.each do |schema|
172
+ dump_schema(schema)
173
+ end
174
+ end
175
+ self << "\nend"
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ end # module Respect
@@ -0,0 +1,20 @@
1
+ module Respect
2
+ class EqualToValidator < Validator
3
+ def initialize(expected)
4
+ @expected = expected
5
+ end
6
+
7
+ def validate(value)
8
+ unless value == @expected
9
+ raise ValidationError,
10
+ "wrong value: `#{value}':#{value.class} instead of `#@expected':#{@expected.class}"
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def to_h_org3
17
+ { 'enum' => [ @expected ] }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,116 @@
1
+ module Respect
2
+ # The evaluation proxy used for the DSL.
3
+ #
4
+ # This class sends all methods it receives to the target object it has
5
+ # received when initialized.
6
+ #
7
+ # If the target's response to +accept_name?+ is +false+, this proxy
8
+ # passes a +nil+ value as first argument to target's dynamic methods
9
+ # and target's methods expecting a name as first argument.
10
+ #
11
+ # This is useful to factor method code that should work in two different
12
+ # contexts. For instance, in the context of a hash schema definition,
13
+ # core statements expect a name as first argument whereas in the context
14
+ # of an array schema definition they do not.
15
+ #
16
+ # HashSchema.define do |s|
17
+ # s.integer "age", greater_than: 0
18
+ # end
19
+ # ArraySchema.define do |s|
20
+ # # FakeNameProxy passes +nil+ here as value for the +name+ argument.
21
+ # s.integer greater_than: 0
22
+ # end
23
+ #
24
+ # To factor this code, we define the +integer+ method in a module included
25
+ # in both context classes.
26
+ #
27
+ # module CoreStatements
28
+ # def integer(name, options = {})
29
+ # update_context name, IntegerSchema.define(options)
30
+ # end
31
+ # end
32
+ # class HashDef
33
+ # include CoreStatements
34
+ # def accept_name?; true; end
35
+ # def update_context(name, schema)
36
+ # @hash_schema[name] = schema
37
+ # end
38
+ # end
39
+ # class ArrayDef
40
+ # include CoreStatements
41
+ # def accept_name?; false; end
42
+ # def update_context(name, schema)
43
+ # @array_schema.item = schema
44
+ # end
45
+ # end
46
+ #
47
+ # The +update_context+ method simply ignored the name argument in ArrayDef
48
+ # because it does not make any sens in this context.
49
+ class FakeNameProxy < BasicObject
50
+
51
+ def initialize(target)
52
+ @target = target
53
+ end
54
+
55
+ # Return the DSL context object where the evaluation takes place.
56
+ attr_reader :target
57
+
58
+ def method_missing(symbol, *args, &block)
59
+ if respond_to_missing?(symbol, false)
60
+ # If the target's default behavior for its methods is to not accept a name as
61
+ # first argument (this is the case for ArrayDef), we introspect the method
62
+ # to decide whether we have to send a nil name as first argument.
63
+ if should_fake_name?
64
+ method = @target.method(symbol)
65
+ else
66
+ method = nil
67
+ end
68
+ # If we have a method and this method is either dynamic or expects a name as its first parameter.
69
+ if method && (dynamic_method?(symbol) || has_name_param?(method))
70
+ args.insert(0, nil)
71
+ begin
72
+ method.call(*args, &block)
73
+ rescue ::ArgumentError => e
74
+ # Decrements argument number mentioned in the message by one.
75
+ message = e.message.sub(/ \((\d+) for (\d+)\)$/) do |match|
76
+ # FIXME(Nicolas Despres): Not sure if this class is still
77
+ # re-entrant since we use $1 and $2. I could not find a way to access
78
+ # to the data matched by sub.
79
+ " (#{$1.to_i - 1} for #{$2.to_i - 1})"
80
+ end
81
+ ::Kernel.raise(::ArgumentError, message)
82
+ end
83
+ else
84
+ @target.public_send(symbol, *args, &block)
85
+ end
86
+ else
87
+ super
88
+ end
89
+ end
90
+
91
+ def respond_to_missing?(symbol, include_all)
92
+ # We ignore include_all since we are only interested in non-private methods.
93
+ @target.respond_to?(symbol)
94
+ end
95
+
96
+ # Evaluate the given +block+ in the context of this class and pass it this
97
+ # instance as argument and returns the value returned by the block.
98
+ def eval(&block)
99
+ block.call(self)
100
+ end
101
+
102
+ private
103
+
104
+ def dynamic_method?(symbol)
105
+ !@target.methods.include?(symbol)
106
+ end
107
+
108
+ def has_name_param?(method)
109
+ !method.parameters.empty? && method.parameters.first.last == :name
110
+ end
111
+
112
+ def should_fake_name?
113
+ @target.respond_to?(:accept_name?) && !@target.accept_name?
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ module Respect
2
+ class FloatSchema < NumericSchema
3
+
4
+ def validate_type(object)
5
+ case object
6
+ when String
7
+ if object =~ /^[-+]?\d+(\.\d+)?$/
8
+ object.to_f
9
+ else
10
+ raise ValidationError,
11
+ "malformed float value: `#{object}'"
12
+ end
13
+ when Float
14
+ object
15
+ when NilClass
16
+ if allow_nil?
17
+ nil
18
+ else
19
+ raise ValidationError, "object is nil but this #{self.class} does not allow nil"
20
+ end
21
+ else
22
+ raise ValidationError, "object is not a float but a '#{object.class}'"
23
+ end
24
+ end
25
+
26
+ end # class FloatSchema
27
+ end # module Respect
@@ -0,0 +1,136 @@
1
+ require 'ipaddr'
2
+ require 'uri'
3
+
4
+ module Respect
5
+ class FormatValidator < Validator
6
+
7
+ PHONE_NUMBER_REGEXP = /^((\+|00)\d{1,2})?\d+$/
8
+
9
+ # FIXME(Nicolas Despres): RFC 1034 mentions that a valid domain can be " "
10
+ # in section 3.5. (see http://www.rfc-editor.org/rfc/rfc1034.txt) but we don't
11
+ # since I don't understand when it is useful.
12
+ HOSTNAME_REGEXP = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/i
13
+
14
+ def initialize(format)
15
+ @format = format
16
+ end
17
+
18
+ def validate(value)
19
+ send("validate_#@format", value)
20
+ end
21
+
22
+ # Validate the given string _value_ describes a well-formed email
23
+ # address following this specification
24
+ # http://www.w3.org/TR/2012/CR-html5-20121217/forms.html#valid-e-mail-address
25
+ def validate_email(value)
26
+ unless value =~ /^[a-zA-Z0-9.!#$\%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
27
+ raise ValidationError, "invalid email address format '#{value}'"
28
+ end
29
+ end
30
+
31
+ # Validate URI string format following RFC 2396
32
+ # (see http://tools.ietf.org/html/rfc2396)
33
+ def validate_uri(value)
34
+ URI.parse(value)
35
+ rescue URI::InvalidURIError => e
36
+ raise ValidationError, "invalid URI: #{e.message}"
37
+ end
38
+
39
+ # Validate the given string _value_ describes a regular expression
40
+ # following the Ruby regular expression syntax.
41
+ def validate_regexp(value)
42
+ Regexp.new(value)
43
+ rescue RegexpError => e
44
+ raise ValidationError, "invalid regexp: #{e.message}"
45
+ end
46
+
47
+ # Validate date and time string format following RFC 3399
48
+ # (see https://tools.ietf.org/html/rfc3339)
49
+ def validate_datetime(value)
50
+ DateTime.rfc3339(value)
51
+ rescue ArgumentError => e
52
+ raise ValidationError, e.message
53
+ end
54
+
55
+ # Validate IPV4 using the standard "ipaddr" ruby module.
56
+ def validate_ipv4_addr(value)
57
+ ipaddr = IPAddr.new(value)
58
+ unless ipaddr.ipv4?
59
+ raise ValidationError, "IP address '#{value}' is not IPv4"
60
+ end
61
+ ipaddr
62
+ rescue ArgumentError => e
63
+ raise ValidationError, "invalid IPv4 address '#{value}' - #{e.message}"
64
+ end
65
+
66
+ # Validate phone number following E.123
67
+ # (see http://en.wikipedia.org/wiki/E.123)
68
+ def validate_phone_number(value)
69
+ unless value =~ PHONE_NUMBER_REGEXP
70
+ raise ValidationError, "invalid phone number '#{value}'"
71
+ end
72
+ end
73
+
74
+ # Validate that the given string _value_ describes a well-formed
75
+ # IPV6 network address using the standard "ipaddr" ruby module.
76
+ def validate_ipv6_addr(value)
77
+ ipaddr = IPAddr.new(value)
78
+ unless ipaddr.ipv6?
79
+ raise ValidationError, "IP address '#{value}' is not IPv6"
80
+ end
81
+ ipaddr
82
+ rescue ArgumentError => e
83
+ raise ValidationError, "invalid IPv6 address '#{value}' - #{e.message}"
84
+ end
85
+
86
+ # Validate that the given string _value_ describes a well-formed
87
+ # IP (IPv6 or IPv4) network address using the standard "ipaddr" ruby module.
88
+ def validate_ip_addr(value)
89
+ IPAddr.new(value)
90
+ rescue ArgumentError => e
91
+ raise ValidationError, "invalid IP address '#{value}' - #{e.message}"
92
+ end
93
+
94
+ # Validate that the given string _value_ describes a well-formed
95
+ # host name as specified by RFC 1034.
96
+ # (see http://www.rfc-editor.org/rfc/rfc1034.txt)
97
+ def validate_hostname(value)
98
+ match_data = HOSTNAME_REGEXP.match(value)
99
+ if match_data
100
+ value.split('.').each_with_index do |label, i|
101
+ unless label.length <= 63
102
+ raise ValidationError,
103
+ "hostname's #{i.ordinalize} label '#{label}' is not less than 63 characters in '#{value}'"
104
+ end
105
+ end
106
+ else
107
+ raise ValidationError, "invalid hostname '#{value}'"
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def to_h_org3
114
+ { 'format' => convert_to_org3_format(@format) }
115
+ end
116
+
117
+ def convert_to_org3_format(format)
118
+ format_type_map = {
119
+ regexp: 'regex',
120
+ datetime: 'date-time',
121
+ ipv4_addr: 'ip-address',
122
+ phone_number: 'phone',
123
+ ipv6_addr: 'ipv6',
124
+ ip_addr: nil,
125
+ hostname: 'host-name',
126
+ }.freeze
127
+ if format_type_map.has_key?(format)
128
+ translation_value = format_type_map[format]
129
+ translation_value unless translation_value.nil?
130
+ else
131
+ format.to_s
132
+ end
133
+ end
134
+
135
+ end # class FormatValidator
136
+ end # module Respect