restspec 0.0.4 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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?
|