apia 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|