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