apia 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/VERSION +1 -0
- data/lib/apia.rb +21 -0
- data/lib/apia/api.rb +100 -0
- data/lib/apia/argument_set.rb +221 -0
- data/lib/apia/authenticator.rb +57 -0
- data/lib/apia/callable_with_environment.rb +43 -0
- data/lib/apia/controller.rb +32 -0
- data/lib/apia/defineable.rb +60 -0
- data/lib/apia/definition.rb +27 -0
- data/lib/apia/definitions/api.rb +51 -0
- data/lib/apia/definitions/argument.rb +77 -0
- data/lib/apia/definitions/argument_set.rb +33 -0
- data/lib/apia/definitions/authenticator.rb +46 -0
- data/lib/apia/definitions/controller.rb +41 -0
- data/lib/apia/definitions/endpoint.rb +74 -0
- data/lib/apia/definitions/enum.rb +31 -0
- data/lib/apia/definitions/error.rb +59 -0
- data/lib/apia/definitions/field.rb +117 -0
- data/lib/apia/definitions/lookup_argument_set.rb +27 -0
- data/lib/apia/definitions/object.rb +29 -0
- data/lib/apia/definitions/polymorph.rb +29 -0
- data/lib/apia/definitions/polymorph_option.rb +53 -0
- data/lib/apia/definitions/scalar.rb +23 -0
- data/lib/apia/definitions/type.rb +109 -0
- data/lib/apia/dsl.rb +23 -0
- data/lib/apia/dsls/api.rb +37 -0
- data/lib/apia/dsls/argument.rb +27 -0
- data/lib/apia/dsls/argument_set.rb +35 -0
- data/lib/apia/dsls/authenticator.rb +38 -0
- data/lib/apia/dsls/concerns/has_fields.rb +38 -0
- data/lib/apia/dsls/controller.rb +34 -0
- data/lib/apia/dsls/endpoint.rb +79 -0
- data/lib/apia/dsls/enum.rb +19 -0
- data/lib/apia/dsls/error.rb +26 -0
- data/lib/apia/dsls/field.rb +27 -0
- data/lib/apia/dsls/lookup_argument_set.rb +24 -0
- data/lib/apia/dsls/object.rb +19 -0
- data/lib/apia/dsls/polymorph.rb +19 -0
- data/lib/apia/dsls/route_group.rb +43 -0
- data/lib/apia/dsls/route_set.rb +40 -0
- data/lib/apia/dsls/scalar.rb +23 -0
- data/lib/apia/dsls/scope_descriptions.rb +17 -0
- data/lib/apia/endpoint.rb +110 -0
- data/lib/apia/enum.rb +43 -0
- data/lib/apia/environment_error_handling.rb +74 -0
- data/lib/apia/error.rb +61 -0
- data/lib/apia/error_set.rb +15 -0
- data/lib/apia/errors/error_exception_error.rb +32 -0
- data/lib/apia/errors/field_spec_parse_error.rb +23 -0
- data/lib/apia/errors/invalid_argument_error.rb +68 -0
- data/lib/apia/errors/invalid_enum_option_error.rb +21 -0
- data/lib/apia/errors/invalid_helper_error.rb +6 -0
- data/lib/apia/errors/invalid_json_error.rb +23 -0
- data/lib/apia/errors/invalid_polymorph_value_error.rb +21 -0
- data/lib/apia/errors/invalid_scalar_value_error.rb +21 -0
- data/lib/apia/errors/manifest_error.rb +43 -0
- data/lib/apia/errors/missing_argument_error.rb +40 -0
- data/lib/apia/errors/null_field_value_error.rb +37 -0
- data/lib/apia/errors/parse_error.rb +10 -0
- data/lib/apia/errors/runtime_error.rb +30 -0
- data/lib/apia/errors/scope_not_granted_error.rb +15 -0
- data/lib/apia/errors/standard_error.rb +6 -0
- data/lib/apia/field_set.rb +76 -0
- data/lib/apia/field_spec.rb +155 -0
- data/lib/apia/helpers.rb +34 -0
- data/lib/apia/hook_set.rb +30 -0
- data/lib/apia/lookup_argument_set.rb +57 -0
- data/lib/apia/lookup_environment.rb +27 -0
- data/lib/apia/manifest_errors.rb +62 -0
- data/lib/apia/mock_request.rb +18 -0
- data/lib/apia/object.rb +68 -0
- data/lib/apia/object_set.rb +21 -0
- data/lib/apia/pagination_object.rb +34 -0
- data/lib/apia/polymorph.rb +50 -0
- data/lib/apia/rack.rb +184 -0
- data/lib/apia/rack_error.rb +17 -0
- data/lib/apia/request.rb +67 -0
- data/lib/apia/request_environment.rb +84 -0
- data/lib/apia/request_headers.rb +42 -0
- data/lib/apia/response.rb +64 -0
- data/lib/apia/route.rb +61 -0
- data/lib/apia/route_group.rb +20 -0
- data/lib/apia/route_set.rb +89 -0
- data/lib/apia/scalar.rb +52 -0
- data/lib/apia/scalars.rb +25 -0
- data/lib/apia/scalars/base64.rb +31 -0
- data/lib/apia/scalars/boolean.rb +37 -0
- data/lib/apia/scalars/date.rb +45 -0
- data/lib/apia/scalars/decimal.rb +36 -0
- data/lib/apia/scalars/integer.rb +34 -0
- data/lib/apia/scalars/string.rb +24 -0
- data/lib/apia/scalars/unix_time.rb +40 -0
- data/lib/apia/schema/api_controller_schema_type.rb +17 -0
- data/lib/apia/schema/api_schema_type.rb +43 -0
- data/lib/apia/schema/argument_schema_type.rb +28 -0
- data/lib/apia/schema/argument_set_schema_type.rb +21 -0
- data/lib/apia/schema/authenticator_schema_type.rb +22 -0
- data/lib/apia/schema/controller.rb +39 -0
- data/lib/apia/schema/controller_endpoint_schema_type.rb +17 -0
- data/lib/apia/schema/controller_schema_type.rb +32 -0
- data/lib/apia/schema/endpoint_schema_type.rb +35 -0
- data/lib/apia/schema/enum_schema_type.rb +20 -0
- data/lib/apia/schema/enum_value_schema_type.rb +14 -0
- data/lib/apia/schema/error_schema_type.rb +23 -0
- data/lib/apia/schema/field_schema_type.rb +38 -0
- data/lib/apia/schema/field_spec_options_schema_type.rb +16 -0
- data/lib/apia/schema/lookup_argument_set_schema_type.rb +25 -0
- data/lib/apia/schema/object_schema_polymorph.rb +31 -0
- data/lib/apia/schema/object_schema_type.rb +21 -0
- data/lib/apia/schema/polymorph_option_schema_type.rb +16 -0
- data/lib/apia/schema/polymorph_schema_type.rb +20 -0
- data/lib/apia/schema/request_method_enum.rb +21 -0
- data/lib/apia/schema/route_group_schema_type.rb +19 -0
- data/lib/apia/schema/route_schema_type.rb +31 -0
- data/lib/apia/schema/route_set_schema_type.rb +20 -0
- data/lib/apia/schema/scalar_schema_type.rb +15 -0
- data/lib/apia/schema/scope_type.rb +14 -0
- data/lib/apia/version.rb +12 -0
- metadata +188 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apia
|
4
|
+
class RequestHeaders
|
5
|
+
|
6
|
+
def initialize(headers)
|
7
|
+
@headers = headers
|
8
|
+
end
|
9
|
+
|
10
|
+
def fetch(key, default = nil)
|
11
|
+
@headers[self.class.make_key(key)] || default
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](key)
|
15
|
+
fetch(key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def []=(key, value)
|
19
|
+
@headers[self.class.make_key(key)] = value
|
20
|
+
end
|
21
|
+
|
22
|
+
class << self
|
23
|
+
|
24
|
+
def make_key(key)
|
25
|
+
key.gsub('-', '_').upcase
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_from_request(request)
|
29
|
+
hash = request.each_header.each_with_object({}) do |(key, value), inner_hash|
|
30
|
+
next unless key =~ /\AHTTP_(\w+)\z/
|
31
|
+
|
32
|
+
name = Regexp.last_match[1]
|
33
|
+
|
34
|
+
inner_hash[name] = value
|
35
|
+
end
|
36
|
+
new(hash)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'apia/rack'
|
5
|
+
|
6
|
+
module Apia
|
7
|
+
class Response
|
8
|
+
|
9
|
+
attr_accessor :status
|
10
|
+
attr_reader :fields
|
11
|
+
attr_reader :headers
|
12
|
+
attr_writer :body
|
13
|
+
|
14
|
+
def initialize(request, endpoint)
|
15
|
+
@request = request
|
16
|
+
@endpoint = endpoint
|
17
|
+
|
18
|
+
@status = @endpoint.definition.http_status_code
|
19
|
+
@fields = {}
|
20
|
+
@headers = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add a field value for this endpoint
|
24
|
+
#
|
25
|
+
# @param name [Symbol]
|
26
|
+
# @param value [Hash, Object, nil]
|
27
|
+
# @return [void]
|
28
|
+
def add_field(name, value)
|
29
|
+
@fields[name.to_sym] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a header to the response
|
33
|
+
#
|
34
|
+
# @param name [String]
|
35
|
+
# @param value [String]
|
36
|
+
# @return [void]
|
37
|
+
def add_header(name, value)
|
38
|
+
@headers[name.to_s] = value&.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return the full hash of data that should be returned for this
|
42
|
+
# request.
|
43
|
+
#
|
44
|
+
# @return [Hash]
|
45
|
+
def hash
|
46
|
+
@hash ||= @endpoint.definition.fields.generate_hash(@fields, request: @request)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return the body that should be returned for this response
|
50
|
+
#
|
51
|
+
# @return [Hash]
|
52
|
+
def body
|
53
|
+
@body || hash
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return the rack triplet for this response
|
57
|
+
#
|
58
|
+
# @return [Array]
|
59
|
+
def rack_triplet
|
60
|
+
Rack.json_triplet(body, headers: @headers, status: @status)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
data/lib/apia/route.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apia/route_set'
|
4
|
+
|
5
|
+
module Apia
|
6
|
+
class Route
|
7
|
+
|
8
|
+
REQUEST_METHODS = [:get, :post, :patch, :put, :delete].freeze
|
9
|
+
|
10
|
+
attr_reader :path
|
11
|
+
attr_reader :controller
|
12
|
+
attr_reader :request_method
|
13
|
+
attr_reader :group
|
14
|
+
attr_writer :endpoint
|
15
|
+
|
16
|
+
def initialize(path, **options)
|
17
|
+
@path = path
|
18
|
+
|
19
|
+
@group = options[:group]
|
20
|
+
|
21
|
+
@controller = options[:controller]
|
22
|
+
@endpoint = options[:endpoint]
|
23
|
+
|
24
|
+
@request_method = options[:request_method] || :get
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return the endpoint object for this route
|
28
|
+
#
|
29
|
+
# @return [Apia::Endpoint]
|
30
|
+
def endpoint
|
31
|
+
if @endpoint.is_a?(Symbol) && controller
|
32
|
+
return controller.definition.endpoints[@endpoint]
|
33
|
+
end
|
34
|
+
|
35
|
+
@endpoint
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return the parts for this route
|
39
|
+
#
|
40
|
+
# @return [Array<String>]
|
41
|
+
def path_parts
|
42
|
+
@path_parts ||= RouteSet.split_path(@path)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extract arguments from the given path and return a hash of the arguments
|
46
|
+
# based on their naming from the route
|
47
|
+
#
|
48
|
+
# @param given_path [String]
|
49
|
+
# @return [Hash]
|
50
|
+
def extract_arguments(given_path)
|
51
|
+
given_path_parts = RouteSet.split_path(given_path)
|
52
|
+
path_parts.each_with_index.each_with_object({}) do |(part, index), hash|
|
53
|
+
next unless part =~ /\A:(\w+)/
|
54
|
+
|
55
|
+
value = given_path_parts[index]
|
56
|
+
hash[Regexp.last_match[1]] = value
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apia
|
4
|
+
class RouteGroup
|
5
|
+
|
6
|
+
attr_reader :id
|
7
|
+
attr_reader :parent
|
8
|
+
attr_accessor :name
|
9
|
+
attr_accessor :description
|
10
|
+
attr_accessor :default_controller
|
11
|
+
attr_reader :groups
|
12
|
+
|
13
|
+
def initialize(id, parent)
|
14
|
+
@id = id
|
15
|
+
@parent = parent
|
16
|
+
@groups = []
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apia/dsls/route_set'
|
4
|
+
|
5
|
+
module Apia
|
6
|
+
class RouteSet
|
7
|
+
|
8
|
+
attr_reader :map
|
9
|
+
attr_reader :routes
|
10
|
+
attr_reader :controllers
|
11
|
+
attr_reader :endpoints
|
12
|
+
attr_reader :groups
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@map = {}
|
16
|
+
@routes = []
|
17
|
+
@controllers = []
|
18
|
+
@endpoints = []
|
19
|
+
@groups = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def dsl
|
23
|
+
@dsl ||= DSLs::RouteSet.new(self)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add a new route to the set
|
27
|
+
#
|
28
|
+
# @param route [Moonstone::Route]
|
29
|
+
# @return [Moonstone::Route]
|
30
|
+
def add(route)
|
31
|
+
@routes << route
|
32
|
+
if route.controller && !@controllers.include?(route.controller)
|
33
|
+
@controllers << route.controller
|
34
|
+
end
|
35
|
+
|
36
|
+
if route.endpoint && !@controllers.include?(route.endpoint)
|
37
|
+
@endpoints << route.endpoint
|
38
|
+
end
|
39
|
+
|
40
|
+
parts = self.class.split_path(route.path).map { |p| p =~ /\A:/ ? '?' : p }
|
41
|
+
parts.size.times do |i|
|
42
|
+
if i.zero?
|
43
|
+
source = @map
|
44
|
+
else
|
45
|
+
source = @map.dig(*parts[0, i])
|
46
|
+
end
|
47
|
+
source[parts[i]] ||= { _routes: [] }
|
48
|
+
source[parts[i]][:_routes] << route if i == parts.size - 1
|
49
|
+
end
|
50
|
+
route
|
51
|
+
end
|
52
|
+
|
53
|
+
# Find routes that exactly match a given path
|
54
|
+
#
|
55
|
+
# @param request_method [Symbol]
|
56
|
+
# @param path [String]
|
57
|
+
# @return [Array<Moonstone::Route>]
|
58
|
+
def find(request_method, path)
|
59
|
+
parts = self.class.split_path(path)
|
60
|
+
last = @map
|
61
|
+
parts.size.times do |i|
|
62
|
+
last = last[parts[i]] || last['?']
|
63
|
+
return [] if last.nil?
|
64
|
+
end
|
65
|
+
last[:_routes].select { |r| r.request_method == request_method }
|
66
|
+
end
|
67
|
+
|
68
|
+
class << self
|
69
|
+
|
70
|
+
# Remove slashes from the start and end of a given string
|
71
|
+
#
|
72
|
+
# @param string [String]
|
73
|
+
# @return [String]
|
74
|
+
def strip_slashes(string)
|
75
|
+
string.sub(/\A\/+/, '').sub(/\/\z/, '')
|
76
|
+
end
|
77
|
+
|
78
|
+
# Split a URL part into its appropriate parts
|
79
|
+
#
|
80
|
+
# @param path [String]
|
81
|
+
# @return [Array<String>]
|
82
|
+
def split_path(path)
|
83
|
+
strip_slashes(path).split('/')
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/lib/apia/scalar.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apia/helpers'
|
4
|
+
require 'apia/definitions/scalar'
|
5
|
+
require 'apia/defineable'
|
6
|
+
|
7
|
+
module Apia
|
8
|
+
class Scalar
|
9
|
+
|
10
|
+
extend Defineable
|
11
|
+
|
12
|
+
# Return the definition for this type
|
13
|
+
#
|
14
|
+
# @return [Apia::Definitions::Object]
|
15
|
+
def self.definition
|
16
|
+
@definition ||= Definitions::Scalar.new(Helpers.class_name_to_id(name))
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.cast(value = nil, &block)
|
20
|
+
if block_given? && value.nil?
|
21
|
+
return definition.dsl.cast(&block)
|
22
|
+
end
|
23
|
+
|
24
|
+
unless valid?(value)
|
25
|
+
# Before casting, we'll also validate...
|
26
|
+
raise InvalidScalarValueError.new(self, value)
|
27
|
+
end
|
28
|
+
|
29
|
+
value = definition.cast.call(value) if definition.cast
|
30
|
+
|
31
|
+
value
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.valid?(value)
|
35
|
+
return true if definition.validator.nil?
|
36
|
+
|
37
|
+
definition.validator.call(value)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.parse(value = nil, &block)
|
41
|
+
if block_given? && value.nil?
|
42
|
+
return definition.dsl.parse(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
return value if definition.parse.nil?
|
46
|
+
return nil if value.nil?
|
47
|
+
|
48
|
+
definition.parse.call(value)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/apia/scalars.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apia
|
4
|
+
module Scalars
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def fetch(item, default = nil)
|
9
|
+
all[item.to_sym] || default
|
10
|
+
end
|
11
|
+
|
12
|
+
def register(name, klass)
|
13
|
+
all[name.to_sym] = klass
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def all
|
19
|
+
@all ||= {}
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apia/scalars'
|
4
|
+
require 'apia/scalar'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module Apia
|
8
|
+
module Scalars
|
9
|
+
class Base64 < Apia::Scalar
|
10
|
+
|
11
|
+
Scalars.register :base64, self
|
12
|
+
|
13
|
+
name 'Base64-encoded string'
|
14
|
+
|
15
|
+
cast do |value|
|
16
|
+
::Base64.encode64(value).sub(/\n\z/, '')
|
17
|
+
end
|
18
|
+
|
19
|
+
validator { true }
|
20
|
+
|
21
|
+
parse do |value|
|
22
|
+
unless value.is_a?(::String)
|
23
|
+
raise Apia::ParseError, 'Base64 value must be provided as a string'
|
24
|
+
end
|
25
|
+
|
26
|
+
::Base64.decode64(value)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apia/scalars'
|
4
|
+
require 'apia/scalar'
|
5
|
+
|
6
|
+
module Apia
|
7
|
+
module Scalars
|
8
|
+
class Boolean < Apia::Scalar
|
9
|
+
|
10
|
+
Scalars.register :boolean, self
|
11
|
+
|
12
|
+
name 'Boolean'
|
13
|
+
|
14
|
+
TRUE_VALUES = [true, 'true', 'yes', 1, '1'].freeze
|
15
|
+
FALSE_VALUES = [false, 'false', 'no', 0, '0'].freeze
|
16
|
+
|
17
|
+
cast do |value|
|
18
|
+
value ? true : false
|
19
|
+
end
|
20
|
+
|
21
|
+
validator do |value|
|
22
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
23
|
+
end
|
24
|
+
|
25
|
+
parse do |value|
|
26
|
+
if TRUE_VALUES.include?(value)
|
27
|
+
true
|
28
|
+
elsif FALSE_VALUES.include?(value)
|
29
|
+
false
|
30
|
+
else
|
31
|
+
raise Apia::ParseError, 'Boolean must be provided as a boolean, as a string containing true or false or as 0 or 1 as an integer.'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'apia/scalars'
|
5
|
+
require 'apia/scalar'
|
6
|
+
require 'apia/errors/parse_error'
|
7
|
+
|
8
|
+
module Apia
|
9
|
+
module Scalars
|
10
|
+
class Date < Apia::Scalar
|
11
|
+
|
12
|
+
Scalars.register :date, self
|
13
|
+
|
14
|
+
name 'Date'
|
15
|
+
|
16
|
+
cast do |value|
|
17
|
+
value.strftime('%Y-%m-%d')
|
18
|
+
end
|
19
|
+
|
20
|
+
validator do |value|
|
21
|
+
value.is_a?(::Date)
|
22
|
+
end
|
23
|
+
|
24
|
+
parse do |string|
|
25
|
+
next string if string.is_a?(::Date)
|
26
|
+
|
27
|
+
begin
|
28
|
+
string = string.to_s
|
29
|
+
unless string =~ /\A\d{4}-\d{2}-\d{2}\z/
|
30
|
+
raise Apia::ParseError, 'Date must be in the format of yyyy-mm-dd'
|
31
|
+
end
|
32
|
+
|
33
|
+
::Date.parse(string)
|
34
|
+
rescue ::ArgumentError => e
|
35
|
+
if e.message =~ /invalid date/
|
36
|
+
raise Apia::ParseError, 'Invalid date was entered (make sure the day exists)'
|
37
|
+
end
|
38
|
+
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|