restspec 0.0.4 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +5 -0
  3. data/CHANGELOG.md +0 -0
  4. data/README.md +11 -7
  5. data/Rakefile +7 -0
  6. data/bin/restspec +1 -1
  7. data/examples/store-api-tests/Gemfile.lock +3 -1
  8. data/examples/store-api-tests/api.md +47 -47
  9. data/examples/store-api-tests/spec/api/product_spec.rb +0 -2
  10. data/examples/store-api-tests/spec/api/restspec/endpoints.rb +6 -5
  11. data/examples/store-api-tests/spec/api/restspec/schemas.rb +2 -1
  12. data/{docs → guides}/endpoints.md +0 -0
  13. data/{docs → guides}/helpers.md +0 -0
  14. data/{docs → guides}/macros.md +0 -0
  15. data/{docs → guides}/matchers.md +0 -0
  16. data/{docs → guides}/schemas.md +0 -0
  17. data/{docs → guides}/tutorial.md +1 -1
  18. data/{docs → guides}/types.md +0 -0
  19. data/lib/restspec/configuration.rb +28 -4
  20. data/lib/restspec/endpoints/dsl.rb +281 -48
  21. data/lib/restspec/endpoints/endpoint.rb +18 -58
  22. data/lib/restspec/endpoints/has_schemas.rb +39 -0
  23. data/lib/restspec/endpoints/namespace.rb +4 -7
  24. data/lib/restspec/endpoints/network.rb +27 -0
  25. data/lib/restspec/endpoints/request.rb +3 -0
  26. data/lib/restspec/endpoints/response.rb +3 -0
  27. data/lib/restspec/endpoints/url_builder.rb +51 -0
  28. data/lib/restspec/rspec/api_macros.rb +2 -2
  29. data/lib/restspec/rspec/matchers/be_like_schema.rb +1 -1
  30. data/lib/restspec/rspec/matchers/be_like_schema_array.rb +1 -1
  31. data/lib/restspec/runners/docs/templates/docs.md.erb +2 -2
  32. data/lib/restspec/schema/attribute.rb +43 -0
  33. data/lib/restspec/schema/attribute_example.rb +13 -1
  34. data/lib/restspec/schema/checker.rb +80 -8
  35. data/lib/restspec/schema/dsl.rb +67 -11
  36. data/lib/restspec/schema/schema.rb +13 -1
  37. data/lib/restspec/schema/schema_example.rb +7 -1
  38. data/lib/restspec/schema/types/array_type.rb +42 -1
  39. data/lib/restspec/schema/types/basic_type.rb +62 -0
  40. data/lib/restspec/schema/types/boolean_type.rb +10 -0
  41. data/lib/restspec/schema/types/date_type.rb +12 -0
  42. data/lib/restspec/schema/types/datetime_type.rb +16 -0
  43. data/lib/restspec/schema/types/decimal_string_type.rb +16 -5
  44. data/lib/restspec/schema/types/decimal_type.rb +17 -1
  45. data/lib/restspec/schema/types/embedded_schema_type.rb +39 -8
  46. data/lib/restspec/schema/types/hash_type.rb +51 -12
  47. data/lib/restspec/schema/types/integer_type.rb +12 -1
  48. data/lib/restspec/schema/types/null_type.rb +7 -0
  49. data/lib/restspec/schema/types/one_of_type.rb +18 -0
  50. data/lib/restspec/schema/types/schema_id_type.rb +14 -17
  51. data/lib/restspec/schema/types/string_type.rb +9 -0
  52. data/lib/restspec/schema/types/type_methods.rb +32 -0
  53. data/lib/restspec/schema/types.rb +1 -18
  54. data/lib/restspec/shortcuts.rb +10 -0
  55. data/lib/restspec/stores/endpoint_store.rb +27 -2
  56. data/lib/restspec/stores/namespace_store.rb +23 -4
  57. data/lib/restspec/stores/schema_store.rb +15 -0
  58. data/lib/restspec/values/status_code.rb +16 -1
  59. data/lib/restspec/version.rb +1 -1
  60. data/lib/restspec.rb +2 -0
  61. data/restspec.gemspec +2 -0
  62. data/spec/restspec/endpoints/dsl_spec.rb +32 -19
  63. data/spec/restspec/endpoints/endpoint_spec.rb +20 -43
  64. data/spec/restspec/endpoints/namespace_spec.rb +0 -7
  65. data/spec/restspec/endpoints/request_spec.rb +33 -0
  66. data/spec/restspec/schema/attribute_spec.rb +44 -0
  67. data/spec/restspec/schema/checker_spec.rb +57 -0
  68. data/spec/restspec/schema/dsl_spec.rb +1 -1
  69. data/spec/restspec/schema/schema_spec.rb +15 -0
  70. data/spec/restspec/schema/types/basic_type_spec.rb +2 -2
  71. data/spec/restspec/schema/types/decimal_string_type_spec.rb +56 -0
  72. data/spec/restspec/schema/types/decimal_type_spec.rb +25 -0
  73. data/spec/restspec/schema/types/embedded_schema_type_spec.rb +32 -0
  74. data/spec/restspec/schema/types/hash_type_spec.rb +39 -0
  75. data/spec/restspec/schema/types/integer_type_spec.rb +28 -0
  76. data/spec/restspec/schema/types/one_of_type_spec.rb +21 -0
  77. data/spec/restspec/stores/endpoint_store_spec.rb +62 -0
  78. metadata +63 -10
  79. data/ROADMAP.md +0 -13
@@ -0,0 +1,51 @@
1
+ module Restspec
2
+ module Endpoints
3
+ class URLBuilder
4
+ attr_reader :url_params
5
+
6
+ PARAM_INTERPOLATION_REGEX = /:([\w]+)/
7
+
8
+ def initialize(path = '', url_params = {}, query_params = {})
9
+ self.path = path
10
+ self.url_params = unbox_url_params(url_params)
11
+ self.query_params = query_params
12
+ end
13
+
14
+ def full_url
15
+ base_url + path_from_params + query_string
16
+ end
17
+
18
+ private
19
+
20
+ attr_accessor :path, :query_params
21
+ attr_writer :url_params
22
+
23
+ def path_from_params
24
+ path.gsub(PARAM_INTERPOLATION_REGEX) do
25
+ url_params[$1] || url_params[$1.to_sym]
26
+ end
27
+ end
28
+
29
+ def base_url
30
+ @base_url ||= (Restspec.config.base_url || '')
31
+ end
32
+
33
+ def query_string
34
+ @query_string ||= fill_query_string(query_params.to_param)
35
+ end
36
+
37
+ def fill_query_string(query_string)
38
+ query_string.present? ? "?#{query_string}" : ""
39
+ end
40
+
41
+ def unbox_url_params(raw_url_params)
42
+ params = raw_url_params.inject({}) do |hash, (key, value)|
43
+ real_value = value.respond_to?(:call) ? value.call : value
44
+ hash.merge(key.to_sym => real_value)
45
+ end
46
+
47
+ Restspec::Values::SuperHash.new(params)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -56,7 +56,7 @@ module Restspec
56
56
 
57
57
  -> do
58
58
  endpoint.execute_once(
59
- body: payload.merge(@payload || {}),
59
+ body: payload,
60
60
  url_params: url_params.merge(resource_params).merge(@url_params || {}),
61
61
  query_params: query_params.merge(@query_params || {}),
62
62
  before: ->do
@@ -72,7 +72,7 @@ module Restspec
72
72
  subject { response }
73
73
 
74
74
  let(:payload) do
75
- defined?(example_payload) ? example_payload : {}
75
+ defined?(example_payload) ? example_payload : nil
76
76
  end
77
77
 
78
78
  let(:url_params) do
@@ -3,7 +3,7 @@ RSpec::Matchers.define :be_like_schema do |schema_name = nil|
3
3
  schema = if schema_name.present?
4
4
  Restspec::SchemaStore.get(schema_name)
5
5
  else
6
- response.endpoint.schema
6
+ response.endpoint.schema_for(:response)
7
7
  end
8
8
 
9
9
  body = response.respond_to?(:body) ? response.body : response
@@ -3,7 +3,7 @@ RSpec::Matchers.define :be_like_schema_array do |schema_name = nil|
3
3
  schema = if schema_name.present?
4
4
  Restspec::SchemaStore.get(schema_name)
5
5
  else
6
- response.endpoint.schema
6
+ response.endpoint.schema_for(:response)
7
7
  end
8
8
 
9
9
  body = response.respond_to?(:body) ? response.body : response
@@ -1,8 +1,8 @@
1
1
  # API
2
2
  ## Hello World
3
3
 
4
- <% namespace_store.each do |namespace| %>
5
- ## <%= namespace.name.capitalize %>
4
+ <% namespace_store.each do |name, namespace| %>
5
+ ## <%= name.capitalize %>
6
6
 
7
7
  <% namespace.all_endpoints.each do |endpoint| %>
8
8
  ### <%= endpoint.name.capitalize %> [<%= endpoint.method.upcase %> <%= endpoint.full_path %>]
@@ -1,8 +1,45 @@
1
1
  module Restspec
2
2
  module Schema
3
+ # An attribute is a part of a schema. All attributes have a name and a type at least.
4
+ # A type is an instance of a subclass of {Restspec::Schema::Types::BasicType} that keeps
5
+ # information about what are valid instances of the attribute and can generate valid
6
+ # instances of the attribute.
7
+ #
8
+ # @example
9
+ #
10
+ # string_type = Types::StringType.new
11
+ # name_attr = Attribute.new(:name, type)
12
+ #
13
+ # string_type.example_for(name_attr) # A random word
14
+ # string_type.valid?(name_attr, 1000) # false
15
+ # string_type.valid?(name_attr, 'John') # true
16
+ #
17
+ # @example With the :example option
18
+ #
19
+ # string_type = Types::StringType.new
20
+ # name_attr = Attribute.new(:name, type, example: 'Example!')
21
+ #
22
+ # string_type.example_for(name_attr) # Example!
23
+ #
3
24
  class Attribute
4
25
  attr_reader :name, :type
5
26
 
27
+ # Creates an attribute. It uses an identifier (name), an instance
28
+ # of a subclass of {Restspec::Schema::Types::BasicType} and a set
29
+ # of options.
30
+ #
31
+ # @param name the name of the attribute
32
+ # @param type an instance of a subclass of {Restspec::Schema::Types::BasicType} that
33
+ # works like the type of this attribute, allowing the type to generate examples and
34
+ # run validations based on this attribute.
35
+ # @param options that can be the following:
36
+ # - **example**: A callable object (eg: a lambda) that returns something.
37
+ # - **for**: Defines what abilities this attributes has.
38
+ # This is an array that can contains none, some or all the symbols
39
+ # `:checks` and `:examples`. This option defaults to `[:checks, :examples]`,
40
+ # allowing the attribute to be used for run validations from {Checker#check!}
41
+ # and for generating examples from {SchemaExample#value}.
42
+ # @return A new instance of Attribtue.
6
43
  def initialize(name, type, options = {})
7
44
  self.name = name
8
45
  self.type = type
@@ -10,14 +47,20 @@ module Restspec
10
47
  self.allowed_abilities = options.fetch(:for, [:checks, :examples])
11
48
  end
12
49
 
50
+ # The inner example in the attribute created calling the :example option
51
+ # when generating examples.
52
+ #
53
+ # @return The inner example created using the :example option.
13
54
  def example
14
55
  @example ||= example_override
15
56
  end
16
57
 
58
+ # @return [true, false] if the attribute has the ability to generate examples or not
17
59
  def can_generate_examples?
18
60
  allowed_abilities.include?(:examples)
19
61
  end
20
62
 
63
+ # @return [true, false] if the attribute has the ability to be checked
21
64
  def can_be_checked?
22
65
  allowed_abilities.include?(:checks)
23
66
  end
@@ -2,7 +2,17 @@ require 'faker'
2
2
 
3
3
  module Restspec
4
4
  module Schema
5
- class AttributeExample < Struct.new(:attribute)
5
+ # Generates an example for a single attribute.
6
+ class AttributeExample
7
+ # Creates a new {AttributeExample} with an {Attribute} object.
8
+ def initialize(attribute)
9
+ self.attribute = attribute
10
+ end
11
+
12
+ # Generates an example using the hardcoded `example_override` option
13
+ # in the attribute or by calling the #example_for method of the type.
14
+ #
15
+ # @return [#as_json] the generated example attribute.
6
16
  def value
7
17
  if attribute.example.present?
8
18
  attribute.example.try(:call) || attribute.example
@@ -13,6 +23,8 @@ module Restspec
13
23
 
14
24
  private
15
25
 
26
+ attr_accessor :attribute
27
+
16
28
  def type
17
29
  attribute.type
18
30
  end
@@ -1,33 +1,105 @@
1
1
  module Restspec
2
2
  module Schema
3
- class Checker < Struct.new(:schema)
3
+ # Checks if a response object (a hash, esentially) is valid against
4
+ # a schema.
5
+ class Checker
6
+ # Creates a new {Checker} using a {Schema} object.
7
+ def initialize(schema)
8
+ self.schema = schema
9
+ end
10
+
11
+ # Checks iteratively through an array of objects.
4
12
  def check_array!(array)
5
13
  array.each { |item| check!(item) }
6
14
  end
7
15
 
16
+ # Checks if an object follows the contract provided by
17
+ # the schema. This will just pass through if everything is ok.
18
+ # If something is wrong, an error will be raised. The actual check
19
+ # will be done, attribute by attribute, by an instance of {ObjectChecker},
20
+ # calling the methods {ObjectChecker#check_missed_key! check_missed_key!} and
21
+ # {ObjectChecker#check_invalid! check_invalid!}.
22
+ #
23
+ # @param object [Hash] the object to check against the schema.
24
+ # @raise NoObjectError if parameter passed is not a hash.
8
25
  def check!(object)
9
26
  raise NoObjectError.new(object) unless object.is_a?(Hash)
10
27
 
11
28
  schema.attributes.each do |_, attribute|
12
29
  if attribute.can_be_checked?
13
30
  checker = ObjectChecker.new(object, attribute)
14
-
15
- raise NoAttributeError.new(object, attribute) if checker.missed_key?
16
- raise DifferentTypeError.new(object, attribute) if checker.wrong_type?
31
+ checker.check_missed_key!
32
+ checker.check_invalid!
17
33
  end
18
34
  end
19
35
  end
20
36
 
21
37
  private
22
38
 
23
- class ObjectChecker < Struct.new(:object, :attribute)
39
+ attr_accessor :schema
40
+
41
+ # Checks an object against a schema's attribute
42
+ # definition.
43
+ class ObjectChecker
44
+ def initialize(object, attribute)
45
+ self.object = object
46
+ self.attribute = attribute
47
+ end
48
+
49
+ # Checks if the attribute's key is absent from the object.
50
+ #
51
+ # @example
52
+ # # Given the following schema
53
+ # schema :product do
54
+ # attribute :name, string
55
+ # end
56
+ #
57
+ # ObjectChecker.new({ age: 10 }, schema.attributes[:name]).missed_key?
58
+ # # true
59
+ # ObjectChecker.new({ name: 'John' }, schema.attributes[:name]).missed_key?
60
+ # # false
61
+ #
62
+ # @return [true, false] If the attribute's key is absent from the object
24
63
  def missed_key?
25
64
  !object.has_key?(attribute.name)
26
65
  end
27
66
 
28
- def wrong_type?
67
+ # Calls {#missed_key?} and if the call is true, raises
68
+ # a {NoAttributeError}.
69
+ def check_missed_key!
70
+ raise NoAttributeError.new(object, attribute) if missed_key?
71
+ end
72
+
73
+ # Checks if the attribute's type validation fails
74
+ # with the object' attribute. To do this, the #valid? method
75
+ # of the type is executed.
76
+ #
77
+ # @example
78
+ # # Given the following schema
79
+ # schema :product do
80
+ # attribute :name, string
81
+ # end
82
+ #
83
+ # ObjectChecker.new({ name: 10 }, schema.attributes[:name]).invalid?
84
+ # # true
85
+ # ObjectChecker.new({ name: 'John' }, schema.attributes[:name]).invalid?
86
+ # # false
87
+ #
88
+ # @return [true, false] If the attribute's type validation fails
89
+ # with the object' attribute.
90
+ def invalid?
29
91
  !attribute.type.totally_valid?(attribute, object.fetch(attribute.name))
30
92
  end
93
+
94
+ # Calls {#invalid?} and if the call is true, raises
95
+ # a {InvalidationError}.
96
+ def check_invalid!
97
+ raise InvalidationError.new(object, attribute) if invalid?
98
+ end
99
+
100
+ private
101
+
102
+ attr_accessor :object, :attribute
31
103
  end
32
104
 
33
105
  class NoAttributeError < StandardError
@@ -43,7 +115,7 @@ module Restspec
43
115
  end
44
116
  end
45
117
 
46
- class DifferentTypeError < StandardError
118
+ class InvalidationError < StandardError
47
119
  attr_accessor :object, :attribute, :value
48
120
 
49
121
  def initialize(object, attribute)
@@ -53,7 +125,7 @@ module Restspec
53
125
  end
54
126
 
55
127
  def to_s
56
- "The property #{attribute.name} of #{object} should be of type #{attribute.type} but it was of type #{value.class}"
128
+ "The property #{attribute.name} of #{object} was not valid according to the type #{attribute.type}"
57
129
  end
58
130
  end
59
131
 
@@ -1,50 +1,106 @@
1
1
  module Restspec
2
2
  module Schema
3
+ # The Schema DSL is what should be used inside the `schemas.rb` file.
4
+ # This class is related to the top-level namespace of the DSL.
3
5
  class DSL
4
- attr_reader :schemas
5
- attr_accessor :mixins
6
-
7
6
  def initialize
8
7
  self.mixins = {}
9
8
  end
10
9
 
10
+ # Generates a schema and sends the schema to an {SingleSchemaDSL}
11
+ # instance for further definitions.
12
+ #
13
+ # @example
14
+ # schema :book do
15
+ # puts self.class # SingleSchemaDSL
16
+ # puts self.schema.class # Schema
17
+ # end
18
+ #
19
+ # @param name {Symbol} the schema's name
20
+ # @param definition A block that will be executed inside the context
21
+ # of a {SingleSchemaDSL} object.
11
22
  def schema(name, &definition)
12
23
  dsl = SingleSchemaDSL.new(name, mixins)
13
24
  dsl.instance_eval(&definition)
14
25
  Restspec::SchemaStore.store(dsl.schema)
15
26
  end
16
27
 
28
+ # Generates a set of calls that can be executed in
29
+ # many schemas with {SingleSchemaDSL#include_attributes}.
30
+ #
31
+ # They are useful to share attributes.
32
+ #
33
+ # @example
34
+ #
35
+ # mixin :timestamps do
36
+ # attribute :created_at, date
37
+ # attribute :updated_at, date
38
+ # end
39
+ #
40
+ # schema :book do
41
+ # include_attributes :timestamps
42
+ # end
43
+ #
44
+ # schema :celphones do
45
+ # include_attributes :timestamps
46
+ # end
47
+ #
48
+ # @param name {Symbol} the mixin's name
49
+ # @param definition A block that will be executed on demand
50
+ # in an {SingleSchemaDSL} object's context.
17
51
  def mixin(name, &definition)
18
52
  mixins[name] = definition
19
53
  end
54
+
55
+ private
56
+
57
+ attr_accessor :mixins
20
58
  end
21
59
 
60
+ # The DSL to use inside `schema` and `mixin` blocks of
61
+ # a {DSL} instance block. It defines specific things of a
62
+ # schema or a group of them.
22
63
  class SingleSchemaDSL
23
- attr_reader :schema, :mixins
64
+ include Types::TypeMethods
65
+
66
+ # @return {Schema} the current schema
67
+ attr_reader :schema
24
68
 
25
69
  def initialize(name, mixins = {})
26
70
  self.schema = Schema.new(name)
27
71
  self.mixins = mixins
28
72
  end
29
73
 
74
+ # Creates an attribute and saving it into the schema.
75
+ # It uses the same parameters as the {Attribute#initialize} method.
76
+ #
77
+ # @example
78
+ #
79
+ # schema :books do
80
+ # attribute :title, string
81
+ # attribute :created_at, datetime, :for => [:checks]
82
+ # end
83
+ #
84
+ # @param (see Attribute#initialize)
30
85
  def attribute(name, type, options = {})
31
86
  new_attribute = Attribute.new(name, type, options)
32
87
  schema.attributes[name.to_s] = new_attribute
33
88
  end
34
89
 
90
+ # Includes a mixin generated by the {DSL#mixin} function
91
+ # into the schema.
92
+ #
93
+ # @example (see DSL#mixin)
94
+ #
95
+ # @param name [Symbol] the mixin name
35
96
  def include_attributes(name)
36
97
  self.instance_eval &mixins.fetch(name)
37
98
  end
38
99
 
39
- Types::ALL.each do |type_name, type_class|
40
- define_method(type_name) do |options = {}|
41
- type_class.new(options)
42
- end
43
- end
44
-
45
100
  private
46
101
 
47
- attr_writer :schema, :mixins
102
+ attr_writer :schema
103
+ attr_accessor :mixins
48
104
  end
49
105
  end
50
106
  end
@@ -1,13 +1,25 @@
1
1
  module Restspec
2
2
  module Schema
3
+ # A schema is a collection of attributes that defines how the data passed through the API
4
+ # should be formed. In REST, they are the representation of the resources the REST API
5
+ # returns.
3
6
  class Schema
4
- attr_reader :name, :attributes
7
+ # The schema identifier.
8
+ attr_reader :name
5
9
 
10
+ # The set of attributes that conforms the schema.
11
+ attr_reader :attributes
12
+
13
+ # @param name [Symbol] The name of the schema
14
+ # @return a new {Restspec::Schema::Schema Schema} object
6
15
  def initialize(name)
7
16
  self.name = name
8
17
  self.attributes = {}
9
18
  end
10
19
 
20
+ # @param without [Array] An array of attributes that should be removed from the schema.
21
+ # This shouldn't be used without cloning first, to avoid modifying a schema
22
+ # used elsewhere.
11
23
  def extend_with(without: [])
12
24
  without.each { |attribute_name| attributes.delete(attribute_name.to_s) }
13
25
  self
@@ -1,13 +1,19 @@
1
1
  module Restspec
2
2
  module Schema
3
+ # A value object that generates a example from a schema using an optional set of extensions.
3
4
  class SchemaExample
4
- attr_accessor :schema, :extensions
5
+ attr_accessor :schema
6
+ attr_accessor :extensions
5
7
 
8
+ # @param schema [Restspec::Schema::Schema] the schema used to generate the example.
9
+ # @param extensions [Hash] A set of extensions to merge with the example.
6
10
  def initialize(schema, extensions = {})
7
11
  self.schema = schema
8
12
  self.extensions = extensions
9
13
  end
10
14
 
15
+ # It returns the generated example.
16
+ # @return [Restspec::Values::SuperHash] generated example.
11
17
  def value
12
18
  attributes.inject({}) do |sample, (_, attribute)|
13
19
  if attribute.can_generate_examples?
@@ -1,5 +1,31 @@
1
1
  module Restspec::Schema::Types
2
2
  class ArrayType < BasicType
3
+ # Generates an example array.
4
+ #
5
+ # @example without a parameterized type
6
+ # # schema
7
+ # attribute :name, array
8
+ # # examples
9
+ # example_for(schema.attributes[:name])
10
+ # # => []
11
+ #
12
+ # @example with a parameterized type and no length example option
13
+ # # schema
14
+ # attribute :name, array.of(string)
15
+ # # examples
16
+ # example_for(schema.attributes[:name])
17
+ # # => ['hola', 'mundo'] # the length is something randomly between 1 a 5.
18
+ #
19
+ # @example with a parameterized type and length example option
20
+ # # schema
21
+ # attribute :name, array(length: 2).of(string) # or:
22
+ # attribute :name, array(example_options: { length: 2}).of(string)
23
+ # # examples
24
+ # example_for(schema.attributes[:name])
25
+ # # => ['hola', 'mundo'] # the length will always be 2
26
+ #
27
+ # @param attribute [Restspec::Schema::Attribute] the atribute of the schema.
28
+ # @return [Array] Generated array for examples.
3
29
  def example_for(attribute)
4
30
  length_only_works_with_parameterized_types!
5
31
 
@@ -8,6 +34,16 @@ module Restspec::Schema::Types
8
34
  end
9
35
  end
10
36
 
37
+ # Validates if the array is valid.
38
+ #
39
+ # - Without a parameterized type, it only checks if the value is an array.
40
+ # - With a parameterized type, it checks is every object inside the array
41
+ # is valid against the parameterized type.
42
+ #
43
+ # @param attribute [Restspec::Schema::Attribute] the atribute of the schema.
44
+ # @param value [Object] the value of the attribute.
45
+ #
46
+ # @return [true, false] If the array is valid.
11
47
  def valid?(attribute, value)
12
48
  is_array = value.is_a?(Array)
13
49
  if parameterized_type
@@ -22,7 +58,12 @@ module Restspec::Schema::Types
22
58
  private
23
59
 
24
60
  def example_length
25
- example_options.fetch(:length, 0)
61
+ example_options.fetch(:length, internal_length)
62
+ end
63
+
64
+ def internal_length
65
+ return 0 if !parameterized_type
66
+ rand(1..5)
26
67
  end
27
68
 
28
69
  def length_only_works_with_parameterized_types!
@@ -1,18 +1,72 @@
1
+ # This is the parent class for all the Types used in the schemas definition.
2
+ # The two main reasons of the inheritance over simple duck typing are:
3
+ #
4
+ # 1. To force the usage of `example_options` and `schema_options`, different
5
+ # sets of options for the two cases a type is used. This two methods are only used
6
+ # privately by the subclasses.
7
+ # 2. To allow some kind of **'type algebra'**, with the {#|}, {#of} and {#totally_valid?} methods.
1
8
  class Restspec::Schema::Types::BasicType
2
9
  def initialize(options = {})
3
10
  self.options = options
4
11
  end
5
12
 
13
+ # The disjunction operator (||) is not a method in ruby, so we are using `|`
14
+ # because it looks similar. The important thing about the type disjunction is
15
+ # that, when checking through a type, a value can checks itself against
16
+ # multiple possible types.
17
+ #
18
+ # @example with two types
19
+ # attribute :name, string | null
20
+ #
21
+ # attr_type = schema.attributes[:name].type
22
+ # attr_type.totally_valid?(schema.attributes[:name], 'Hola') # true
23
+ # attr_type.totally_valid?(schema.attributes[:name], nil) # true
24
+ # attr_type.totally_valid?(schema.attributes[:name], 10) # false
25
+ #
26
+ # The example works because the type returned by `string | null` is basically
27
+ # just `string` with a disjunction set to `null`. When validating, if the validation
28
+ # fails initially, the disjunction is used as a second source of thuth. Because the
29
+ # disjunction can have disjunctions too, we can test against more than two types.
30
+ #
31
+ # @example with more than two types
32
+ # attribute :name, string | (null | integer)
33
+ #
34
+ # attr_type = schema.attributes[:name].type
35
+ # attr_type.totally_valid?(schema.attributes[:name], 'Hola') # true
36
+ # attr_type.totally_valid?(schema.attributes[:name], nil) # true
37
+ # attr_type.totally_valid?(schema.attributes[:name], 10) # true
38
+ #
39
+ # @param other_type [instance of subclass of BasicType] the type to make the disjuction.
40
+ # @return [BasicType] the same object that is used to call the method. (`self`)
6
41
  def |(other_type)
7
42
  self.disjuction = other_type
8
43
  self
9
44
  end
10
45
 
46
+ # The only work of `of` is to set a `parameterized_type` attribute
47
+ # on the type. This parameterized type can be used by the type itself to
48
+ # whatever the type wants. The major limitation is that, by now, we only
49
+ # allow one parameterized type. For example, {ArrayType} uses this parameterized
50
+ # type to do this:
51
+ #
52
+ # @example
53
+ # attribute :codes, array.of(integer)
54
+ #
55
+ # @param other_type [instance of subclass of BasicType] the type to make the
56
+ # save as the parameterized type.
57
+ # @return [BasicType] the same object that is used to call the method. (`self`)
11
58
  def of(other_type)
12
59
  self.parameterized_type = other_type
13
60
  self
14
61
  end
15
62
 
63
+ # This calls the `valid?` method (that is not present in this class but should
64
+ # be present on their children) making sure to fallback to the disjunction if
65
+ # the disjunction is present.
66
+ #
67
+ # @param attribute [Restspec::Schema::Attribute] The attribute to use.
68
+ # @param value [Object] the object that holds the actual value to test against.
69
+ # @return [true, false] If the type is valid with the following attribute and value.
16
70
  def totally_valid?(attribute, value)
17
71
  if disjuction.present?
18
72
  valid?(attribute, value) || disjuction.valid?(attribute, value)
@@ -21,6 +75,14 @@ class Restspec::Schema::Types::BasicType
21
75
  end
22
76
  end
23
77
 
78
+ # @return [String] a string representation of the type. It's basically the
79
+ # class name without the `Type` postfix underscorized.
80
+ #
81
+ # @example
82
+ #
83
+ # StringType.new.to_s #=> string
84
+ # ArrayType.new.to_s #=> array
85
+ # SchemaIdType.new.to_s #=> schema_id
24
86
  def to_s
25
87
  self.class.name.demodulize.gsub(/Type$/, "").underscore
26
88
  end
@@ -1,9 +1,19 @@
1
1
  module Restspec::Schema::Types
2
2
  class BooleanType < BasicType
3
+ # Generates an example boolean.
4
+ #
5
+ # @param attribute [Restspec::Schema::Attribute] the atribute of the schema.
6
+ # @return [true, false] One of `true` and `false`, randomly.
3
7
  def example_for(attribute)
4
8
  [true, false].sample
5
9
  end
6
10
 
11
+ # Validates is the value is a boolean.
12
+ #
13
+ # @param attribute [Restspec::Schema::Attribute] the atribute of the schema.
14
+ # @param value [Object] the value of the attribute.
15
+ #
16
+ # @return [true, false] If the value is one of true and false.
7
17
  def valid?(attribute, value)
8
18
  [true, false].include?(value)
9
19
  end
@@ -2,10 +2,22 @@ module Restspec::Schema::Types
2
2
  class DateType < BasicType
3
3
  DATE_FORMAT = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/
4
4
 
5
+ # Generates an example date.
6
+ #
7
+ # @param attribute [Restspec::Schema::Attribute] the atribute of the schema.
8
+ # @return [Date] A random date between one month ago and today.
5
9
  def example_for(attribute)
6
10
  Faker::Date.between(1.month.ago, Date.today).to_s
7
11
  end
8
12
 
13
+ # Validates if the value is a date.
14
+ # It basically checks if the date is according
15
+ # to yyyy-mm-dd format
16
+ #
17
+ # @param attribute [Restspec::Schema::Attribute] the atribute of the schema.
18
+ # @param value [Object] the value of the attribute.
19
+ #
20
+ # @return [true, false] If the value is a date with the correct format.
9
21
  def valid?(attribute, value)
10
22
  return false unless value.present?
11
23
  return false unless value.match(DATE_FORMAT).present?