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.
- checksums.yaml +4 -4
- data/.yardopts +5 -0
- data/CHANGELOG.md +0 -0
- data/README.md +11 -7
- data/Rakefile +7 -0
- data/bin/restspec +1 -1
- data/examples/store-api-tests/Gemfile.lock +3 -1
- data/examples/store-api-tests/api.md +47 -47
- data/examples/store-api-tests/spec/api/product_spec.rb +0 -2
- data/examples/store-api-tests/spec/api/restspec/endpoints.rb +6 -5
- data/examples/store-api-tests/spec/api/restspec/schemas.rb +2 -1
- data/{docs → guides}/endpoints.md +0 -0
- data/{docs → guides}/helpers.md +0 -0
- data/{docs → guides}/macros.md +0 -0
- data/{docs → guides}/matchers.md +0 -0
- data/{docs → guides}/schemas.md +0 -0
- data/{docs → guides}/tutorial.md +1 -1
- data/{docs → guides}/types.md +0 -0
- data/lib/restspec/configuration.rb +28 -4
- data/lib/restspec/endpoints/dsl.rb +281 -48
- data/lib/restspec/endpoints/endpoint.rb +18 -58
- data/lib/restspec/endpoints/has_schemas.rb +39 -0
- data/lib/restspec/endpoints/namespace.rb +4 -7
- data/lib/restspec/endpoints/network.rb +27 -0
- data/lib/restspec/endpoints/request.rb +3 -0
- data/lib/restspec/endpoints/response.rb +3 -0
- data/lib/restspec/endpoints/url_builder.rb +51 -0
- data/lib/restspec/rspec/api_macros.rb +2 -2
- data/lib/restspec/rspec/matchers/be_like_schema.rb +1 -1
- data/lib/restspec/rspec/matchers/be_like_schema_array.rb +1 -1
- data/lib/restspec/runners/docs/templates/docs.md.erb +2 -2
- data/lib/restspec/schema/attribute.rb +43 -0
- data/lib/restspec/schema/attribute_example.rb +13 -1
- data/lib/restspec/schema/checker.rb +80 -8
- data/lib/restspec/schema/dsl.rb +67 -11
- data/lib/restspec/schema/schema.rb +13 -1
- data/lib/restspec/schema/schema_example.rb +7 -1
- data/lib/restspec/schema/types/array_type.rb +42 -1
- data/lib/restspec/schema/types/basic_type.rb +62 -0
- data/lib/restspec/schema/types/boolean_type.rb +10 -0
- data/lib/restspec/schema/types/date_type.rb +12 -0
- data/lib/restspec/schema/types/datetime_type.rb +16 -0
- data/lib/restspec/schema/types/decimal_string_type.rb +16 -5
- data/lib/restspec/schema/types/decimal_type.rb +17 -1
- data/lib/restspec/schema/types/embedded_schema_type.rb +39 -8
- data/lib/restspec/schema/types/hash_type.rb +51 -12
- data/lib/restspec/schema/types/integer_type.rb +12 -1
- data/lib/restspec/schema/types/null_type.rb +7 -0
- data/lib/restspec/schema/types/one_of_type.rb +18 -0
- data/lib/restspec/schema/types/schema_id_type.rb +14 -17
- data/lib/restspec/schema/types/string_type.rb +9 -0
- data/lib/restspec/schema/types/type_methods.rb +32 -0
- data/lib/restspec/schema/types.rb +1 -18
- data/lib/restspec/shortcuts.rb +10 -0
- data/lib/restspec/stores/endpoint_store.rb +27 -2
- data/lib/restspec/stores/namespace_store.rb +23 -4
- data/lib/restspec/stores/schema_store.rb +15 -0
- data/lib/restspec/values/status_code.rb +16 -1
- data/lib/restspec/version.rb +1 -1
- data/lib/restspec.rb +2 -0
- data/restspec.gemspec +2 -0
- data/spec/restspec/endpoints/dsl_spec.rb +32 -19
- data/spec/restspec/endpoints/endpoint_spec.rb +20 -43
- data/spec/restspec/endpoints/namespace_spec.rb +0 -7
- data/spec/restspec/endpoints/request_spec.rb +33 -0
- data/spec/restspec/schema/attribute_spec.rb +44 -0
- data/spec/restspec/schema/checker_spec.rb +57 -0
- data/spec/restspec/schema/dsl_spec.rb +1 -1
- data/spec/restspec/schema/schema_spec.rb +15 -0
- data/spec/restspec/schema/types/basic_type_spec.rb +2 -2
- data/spec/restspec/schema/types/decimal_string_type_spec.rb +56 -0
- data/spec/restspec/schema/types/decimal_type_spec.rb +25 -0
- data/spec/restspec/schema/types/embedded_schema_type_spec.rb +32 -0
- data/spec/restspec/schema/types/hash_type_spec.rb +39 -0
- data/spec/restspec/schema/types/integer_type_spec.rb +28 -0
- data/spec/restspec/schema/types/one_of_type_spec.rb +21 -0
- data/spec/restspec/stores/endpoint_store_spec.rb +62 -0
- metadata +63 -10
- 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
|
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.
|
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.
|
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
|
-
## <%=
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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}
|
128
|
+
"The property #{attribute.name} of #{object} was not valid according to the type #{attribute.type}"
|
57
129
|
end
|
58
130
|
end
|
59
131
|
|
data/lib/restspec/schema/dsl.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
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
|
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,
|
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?
|