restspec 0.0.4 → 0.1

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