haveapi 0.26.5 → 0.27.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 +4 -4
- data/.rspec +1 -0
- data/Gemfile +7 -3
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/action.rb +1 -1
- data/lib/haveapi/authentication/base.rb +2 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +10 -2
- data/lib/haveapi/authentication/token/provider.rb +25 -1
- data/lib/haveapi/model_adapters/active_record.rb +61 -4
- data/lib/haveapi/parameters/typed.rb +94 -10
- data/lib/haveapi/params.rb +6 -1
- data/lib/haveapi/resource.rb +1 -1
- data/lib/haveapi/server.rb +10 -1
- data/lib/haveapi/spec/api_builder.rb +8 -3
- data/lib/haveapi/spec/spec_methods.rb +20 -10
- data/lib/haveapi/version.rb +1 -1
- data/spec/action/authorize_spec.rb +317 -0
- data/spec/action/dsl_spec.rb +98 -100
- data/spec/action/runtime_spec.rb +207 -0
- data/spec/action_state_spec.rb +301 -0
- data/spec/authentication/basic_spec.rb +108 -0
- data/spec/authentication/oauth2_spec.rb +127 -0
- data/spec/authentication/token_spec.rb +233 -0
- data/spec/authorization_spec.rb +23 -18
- data/spec/common_spec.rb +19 -17
- data/spec/documentation/auth_filtering_spec.rb +111 -0
- data/spec/documentation_spec.rb +165 -2
- data/spec/envelope_spec.rb +5 -9
- data/spec/extensions/action_exceptions_spec.rb +163 -0
- data/spec/hooks_spec.rb +32 -38
- data/spec/model_adapters/active_record_spec.rb +411 -0
- data/spec/parameters/typed_spec.rb +54 -1
- data/spec/params_spec.rb +27 -25
- data/spec/resource_spec.rb +36 -22
- data/spec/server/integration_spec.rb +71 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/validators/acceptance_spec.rb +10 -12
- data/spec/validators/confirmation_spec.rb +14 -16
- data/spec/validators/custom_spec.rb +1 -1
- data/spec/validators/exclusion_spec.rb +13 -15
- data/spec/validators/format_spec.rb +20 -22
- data/spec/validators/inclusion_spec.rb +13 -15
- data/spec/validators/length_spec.rb +6 -6
- data/spec/validators/numericality_spec.rb +10 -10
- data/spec/validators/presence_spec.rb +16 -22
- data/test_support/client_test_api.rb +583 -0
- data/test_support/client_test_server.rb +59 -0
- metadata +16 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8307e5e40983cf927641fd3d4052058406209093520a83d89121883f64a9ba0e
|
|
4
|
+
data.tar.gz: b2d80c7621b7f8870e89dff0ae78c3f4f2d5587ddc9151733d795df0c279fbb7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b93b9411fb84cf5e67763e9384746da97f32903126e802ca2a47f545f68c9d37f66c54c280e78bc7b25e9abad19ca99e3a6d9ed68a985daa8c053119d459ed6d
|
|
7
|
+
data.tar.gz: 06fff468dc3e939c838689fbd515ae280cf590c9a05d1c36dc81ba7c2723ab344acadee67ece39fb782ff10c75baf7be906fa2e786e5f7cf9a9ae47fe110d9f1
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/Gemfile
CHANGED
|
@@ -5,10 +5,14 @@ gem 'haveapi-client', path: '../../clients/ruby'
|
|
|
5
5
|
|
|
6
6
|
group :test do
|
|
7
7
|
gem 'rack-test'
|
|
8
|
+
gem 'rackup'
|
|
8
9
|
gem 'rspec'
|
|
9
|
-
|
|
10
|
+
gem 'rubocop', require: false
|
|
11
|
+
gem 'rubocop-rspec', require: false
|
|
12
|
+
gem 'webrick'
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
gem 'activerecord', '>=
|
|
14
|
+
# ActiveRecord adapter specs (always run)
|
|
15
|
+
gem 'activerecord', '>= 8.0'
|
|
13
16
|
gem 'sinatra-activerecord'
|
|
17
|
+
gem 'sqlite3'
|
|
14
18
|
end
|
data/haveapi.gemspec
CHANGED
|
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
|
|
|
15
15
|
s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
|
|
16
16
|
|
|
17
17
|
s.add_dependency 'activesupport', '>= 7.1'
|
|
18
|
-
s.add_dependency 'haveapi-client', '~> 0.
|
|
18
|
+
s.add_dependency 'haveapi-client', '~> 0.27.0'
|
|
19
19
|
s.add_dependency 'json'
|
|
20
20
|
s.add_dependency 'mail'
|
|
21
21
|
s.add_dependency 'nesty', '~> 1.0'
|
data/lib/haveapi/action.rb
CHANGED
|
@@ -117,7 +117,7 @@ module HaveAPI::Authentication
|
|
|
117
117
|
request.params['access_token'],
|
|
118
118
|
token_from_authorization_header(request),
|
|
119
119
|
token_from_haveapi_header(request)
|
|
120
|
-
].
|
|
120
|
+
].select { |value| token_present?(value) }
|
|
121
121
|
|
|
122
122
|
token =
|
|
123
123
|
case tokens.length
|
|
@@ -126,7 +126,8 @@ module HaveAPI::Authentication
|
|
|
126
126
|
when 1
|
|
127
127
|
tokens.first
|
|
128
128
|
else
|
|
129
|
-
raise
|
|
129
|
+
raise HaveAPI::Authentication::TokenConflict,
|
|
130
|
+
'Multiple OAuth2 tokens provided'
|
|
130
131
|
end
|
|
131
132
|
|
|
132
133
|
token && config.find_user_by_access_token(request, token)
|
|
@@ -310,6 +311,13 @@ module HaveAPI::Authentication
|
|
|
310
311
|
def header_to_env(header)
|
|
311
312
|
"HTTP_#{header.upcase.gsub('-', '_')}"
|
|
312
313
|
end
|
|
314
|
+
|
|
315
|
+
def token_present?(value)
|
|
316
|
+
return false if value.nil?
|
|
317
|
+
return false if value.respond_to?(:empty?) && value.empty?
|
|
318
|
+
|
|
319
|
+
true
|
|
320
|
+
end
|
|
313
321
|
end
|
|
314
322
|
end
|
|
315
323
|
end
|
|
@@ -159,7 +159,24 @@ module HaveAPI::Authentication
|
|
|
159
159
|
# @param request [Sinatra::Request]
|
|
160
160
|
# @return [String]
|
|
161
161
|
def token(request)
|
|
162
|
-
|
|
162
|
+
param = config.class.query_parameter
|
|
163
|
+
query_token = [
|
|
164
|
+
request.params[param],
|
|
165
|
+
request.params[param.to_s]
|
|
166
|
+
].find { |value| token_present?(value) }
|
|
167
|
+
header_token = request.env[header_to_env]
|
|
168
|
+
|
|
169
|
+
tokens = [query_token, header_token].select { |value| token_present?(value) }
|
|
170
|
+
|
|
171
|
+
case tokens.length
|
|
172
|
+
when 0
|
|
173
|
+
nil
|
|
174
|
+
when 1
|
|
175
|
+
tokens.first
|
|
176
|
+
else
|
|
177
|
+
raise HaveAPI::Authentication::TokenConflict,
|
|
178
|
+
'Multiple authentication tokens provided'
|
|
179
|
+
end
|
|
163
180
|
end
|
|
164
181
|
|
|
165
182
|
def describe
|
|
@@ -176,6 +193,13 @@ module HaveAPI::Authentication
|
|
|
176
193
|
"HTTP_#{config.class.http_header.upcase.gsub('-', '_')}"
|
|
177
194
|
end
|
|
178
195
|
|
|
196
|
+
def token_present?(value)
|
|
197
|
+
return false if value.nil?
|
|
198
|
+
return false if value.respond_to?(:empty?) && value.empty?
|
|
199
|
+
|
|
200
|
+
true
|
|
201
|
+
end
|
|
202
|
+
|
|
179
203
|
def token_resource
|
|
180
204
|
provider = self
|
|
181
205
|
|
|
@@ -142,12 +142,69 @@ module HaveAPI::ModelAdapters
|
|
|
142
142
|
|
|
143
143
|
class Input < ::HaveAPI::ModelAdapter::Input
|
|
144
144
|
def self.clean(model, raw, extra)
|
|
145
|
-
return if
|
|
145
|
+
return nil if raw.nil?
|
|
146
|
+
|
|
147
|
+
original = raw
|
|
148
|
+
|
|
149
|
+
if raw.is_a?(String)
|
|
150
|
+
stripped = raw.strip
|
|
151
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}" if stripped.empty?
|
|
152
|
+
|
|
153
|
+
raw = stripped
|
|
154
|
+
|
|
155
|
+
elsif [true, false].include?(raw)
|
|
156
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
value = if integer_pk?(model)
|
|
160
|
+
id = coerce_integer_id(raw, original)
|
|
161
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}" if id < 0
|
|
162
|
+
|
|
163
|
+
id
|
|
164
|
+
else
|
|
165
|
+
raw
|
|
166
|
+
end
|
|
146
167
|
|
|
147
168
|
if extra[:fetch]
|
|
148
|
-
model.instance_exec(
|
|
169
|
+
model.instance_exec(value, &extra[:fetch])
|
|
170
|
+
else
|
|
171
|
+
model.find(value)
|
|
172
|
+
end
|
|
173
|
+
rescue ::ActiveRecord::RecordNotFound
|
|
174
|
+
raise HaveAPI::ValidationError, 'resource not found'
|
|
175
|
+
rescue ArgumentError, TypeError
|
|
176
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def self.integer_pk?(model)
|
|
180
|
+
pk = model.primary_key
|
|
181
|
+
return false unless pk.is_a?(String)
|
|
182
|
+
return false unless model.respond_to?(:type_for_attribute)
|
|
183
|
+
|
|
184
|
+
pk_type = model.type_for_attribute(pk).type
|
|
185
|
+
%i[integer bigint].include?(pk_type)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def self.coerce_integer_id(raw, original)
|
|
189
|
+
case raw
|
|
190
|
+
when Integer
|
|
191
|
+
raw
|
|
192
|
+
when Float
|
|
193
|
+
unless raw.finite? && (raw % 1) == 0
|
|
194
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
raw.to_i
|
|
198
|
+
when String
|
|
199
|
+
s = raw.strip
|
|
200
|
+
|
|
201
|
+
if s.empty? || !s.match?(/\A[+-]?\d+\z/)
|
|
202
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
Integer(s, 10)
|
|
149
206
|
else
|
|
150
|
-
|
|
207
|
+
raise HaveAPI::ValidationError, "not a valid id #{original.inspect}"
|
|
151
208
|
end
|
|
152
209
|
end
|
|
153
210
|
end
|
|
@@ -397,7 +454,7 @@ module HaveAPI::ModelAdapters
|
|
|
397
454
|
opts[:min] = v.options[:greater_than_or_equal_to] if v.options[:greater_than_or_equal_to]
|
|
398
455
|
|
|
399
456
|
if v.options[:equal_to]
|
|
400
|
-
validator(accept
|
|
457
|
+
validator(:accept, v.options[:equal_to])
|
|
401
458
|
next
|
|
402
459
|
end
|
|
403
460
|
|
|
@@ -33,7 +33,7 @@ module HaveAPI::Parameters
|
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def optional?
|
|
36
|
-
|
|
36
|
+
!required?
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def fill?
|
|
@@ -82,20 +82,19 @@ module HaveAPI::Parameters
|
|
|
82
82
|
nil
|
|
83
83
|
|
|
84
84
|
elsif @type == Integer
|
|
85
|
-
raw
|
|
85
|
+
coerce_integer(raw)
|
|
86
86
|
|
|
87
87
|
elsif @type == Float
|
|
88
|
-
raw
|
|
88
|
+
coerce_float(raw)
|
|
89
89
|
|
|
90
90
|
elsif @type == Boolean
|
|
91
|
-
|
|
91
|
+
coerce_boolean(raw)
|
|
92
92
|
|
|
93
93
|
elsif @type == ::Datetime
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
94
|
+
coerce_datetime(raw)
|
|
95
|
+
|
|
96
|
+
elsif @type == String || @type == Text
|
|
97
|
+
coerce_string(raw)
|
|
99
98
|
|
|
100
99
|
else
|
|
101
100
|
raw
|
|
@@ -122,12 +121,97 @@ module HaveAPI::Parameters
|
|
|
122
121
|
elsif @type == Float
|
|
123
122
|
v.to_f
|
|
124
123
|
|
|
125
|
-
elsif @type == String
|
|
124
|
+
elsif @type == String || @type == Text
|
|
126
125
|
v.to_s
|
|
127
126
|
|
|
128
127
|
else
|
|
129
128
|
v
|
|
130
129
|
end
|
|
131
130
|
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def coerce_integer(raw)
|
|
135
|
+
case raw
|
|
136
|
+
when Integer
|
|
137
|
+
raw
|
|
138
|
+
when Float
|
|
139
|
+
unless raw.finite? && (raw % 1) == 0
|
|
140
|
+
raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raw.to_i
|
|
144
|
+
when String
|
|
145
|
+
s = raw.strip
|
|
146
|
+
|
|
147
|
+
if s.empty? || !s.match?(/\A[+-]?\d+\z/)
|
|
148
|
+
raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
Integer(s, 10)
|
|
152
|
+
else
|
|
153
|
+
raise HaveAPI::ValidationError, "not a valid integer #{raw.inspect}"
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def coerce_float(raw)
|
|
158
|
+
if raw.is_a?(Numeric)
|
|
159
|
+
f = raw.to_f
|
|
160
|
+
|
|
161
|
+
elsif raw.is_a?(String)
|
|
162
|
+
s = raw.strip
|
|
163
|
+
raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" if s.empty?
|
|
164
|
+
|
|
165
|
+
begin
|
|
166
|
+
f = Float(s)
|
|
167
|
+
rescue ArgumentError
|
|
168
|
+
raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
else
|
|
172
|
+
raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
raise HaveAPI::ValidationError, "not a valid float #{raw.inspect}" unless f.finite?
|
|
176
|
+
|
|
177
|
+
f
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def coerce_boolean(raw)
|
|
181
|
+
return true if raw == true
|
|
182
|
+
return false if raw == false
|
|
183
|
+
|
|
184
|
+
if raw.is_a?(Integer)
|
|
185
|
+
return false if raw == 0
|
|
186
|
+
return true if raw == 1
|
|
187
|
+
|
|
188
|
+
elsif raw.is_a?(String)
|
|
189
|
+
s = raw.strip
|
|
190
|
+
raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}" if s.empty?
|
|
191
|
+
|
|
192
|
+
return true if %w[true t yes y 1].include?(s.downcase)
|
|
193
|
+
return false if %w[false f no n 0].include?(s.downcase)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
raise HaveAPI::ValidationError, "not a valid boolean #{raw.inspect}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def coerce_datetime(raw)
|
|
200
|
+
if raw.is_a?(String) && raw.strip.empty?
|
|
201
|
+
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
DateTime.iso8601(raw).to_time
|
|
205
|
+
rescue ArgumentError
|
|
206
|
+
raise HaveAPI::ValidationError, "not in ISO 8601 format '#{raw}'"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def coerce_string(raw)
|
|
210
|
+
if raw.is_a?(Array) || raw.is_a?(Hash)
|
|
211
|
+
raise HaveAPI::ValidationError, "not a valid string #{raw.inspect}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
raw.to_s
|
|
215
|
+
end
|
|
132
216
|
end
|
|
133
217
|
end
|
data/lib/haveapi/params.rb
CHANGED
|
@@ -64,7 +64,12 @@ module HaveAPI
|
|
|
64
64
|
return @cache[:namespace] = @namespace unless @namespace.nil?
|
|
65
65
|
|
|
66
66
|
n = @action.resource.resource_name.underscore
|
|
67
|
-
n =
|
|
67
|
+
n = if %i[object_list hash_list].include?(layout)
|
|
68
|
+
n.pluralize
|
|
69
|
+
else
|
|
70
|
+
n.singularize
|
|
71
|
+
end
|
|
72
|
+
|
|
68
73
|
@cache[:namespace] = n.to_sym
|
|
69
74
|
end
|
|
70
75
|
|
data/lib/haveapi/resource.rb
CHANGED
data/lib/haveapi/server.rb
CHANGED
|
@@ -65,7 +65,16 @@ module HaveAPI
|
|
|
65
65
|
def authenticated?(v)
|
|
66
66
|
return @current_user if @current_user
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
begin
|
|
69
|
+
@current_user = settings.api_server.send(:do_authenticate, v, request)
|
|
70
|
+
rescue HaveAPI::Authentication::TokenConflict => e
|
|
71
|
+
unless @formatter
|
|
72
|
+
@formatter = OutputFormatter.new
|
|
73
|
+
@formatter.supports?([])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
report_error(400, {}, e.message)
|
|
77
|
+
end
|
|
69
78
|
settings.api_server.call_hooks_for(:post_authenticated, args: [@current_user])
|
|
70
79
|
@current_user
|
|
71
80
|
end
|
|
@@ -14,7 +14,7 @@ module HaveAPI::Spec
|
|
|
14
14
|
def api(mod = nil, &block)
|
|
15
15
|
unless mod
|
|
16
16
|
mod = Module.new do
|
|
17
|
-
def self.define_resource(name, superclass: Resource, &block)
|
|
17
|
+
def self.define_resource(name, superclass: HaveAPI::Resource, &block)
|
|
18
18
|
return false if const_defined?(name)
|
|
19
19
|
|
|
20
20
|
cls = Class.new(superclass)
|
|
@@ -39,7 +39,7 @@ module HaveAPI::Spec
|
|
|
39
39
|
|
|
40
40
|
# Select API versions to be used.
|
|
41
41
|
def use_version(v)
|
|
42
|
-
before
|
|
42
|
+
before do
|
|
43
43
|
self.class.opt(:versions, v)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -49,6 +49,11 @@ module HaveAPI::Spec
|
|
|
49
49
|
opt(:default_version, v)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Set action state backend to mount HaveAPI::Resources::ActionState
|
|
53
|
+
def action_state(backend)
|
|
54
|
+
opt(:action_state, backend)
|
|
55
|
+
end
|
|
56
|
+
|
|
52
57
|
# Set a custom mount path.
|
|
53
58
|
def mount_to(path)
|
|
54
59
|
opt(:mount, path)
|
|
@@ -56,7 +61,7 @@ module HaveAPI::Spec
|
|
|
56
61
|
|
|
57
62
|
# Login using HTTP basic.
|
|
58
63
|
def login(*credentials)
|
|
59
|
-
before
|
|
64
|
+
before do
|
|
60
65
|
basic_authorize(*credentials)
|
|
61
66
|
end
|
|
62
67
|
end
|
|
@@ -13,6 +13,8 @@ module HaveAPI::Spec
|
|
|
13
13
|
@api.auth_chain << auth if auth
|
|
14
14
|
@api.use_version(get_opt(:versions) || :all)
|
|
15
15
|
@api.default_version = default if default
|
|
16
|
+
as = get_opt(:action_state)
|
|
17
|
+
@api.action_state = as if as
|
|
16
18
|
@api.mount(get_opt(:mount) || '/')
|
|
17
19
|
@api.app
|
|
18
20
|
end
|
|
@@ -39,20 +41,28 @@ module HaveAPI::Spec
|
|
|
39
41
|
r_name, a_name
|
|
40
42
|
)
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
json_body = params && params.to_json
|
|
45
|
+
env = { 'CONTENT_TYPE' => 'application/json' }
|
|
46
|
+
|
|
47
|
+
if %i[get head options].include?(action.http_method.to_sym)
|
|
48
|
+
method(action.http_method).call(path, params, env)
|
|
49
|
+
else
|
|
50
|
+
env[:input] = json_body if json_body
|
|
51
|
+
method(action.http_method).call(path, nil, env)
|
|
52
|
+
end
|
|
47
53
|
|
|
48
54
|
else
|
|
49
55
|
http_method, path, params = args
|
|
50
56
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
json_body = params && params.to_json
|
|
58
|
+
env = { 'CONTENT_TYPE' => 'application/json' }
|
|
59
|
+
|
|
60
|
+
if %i[get head options].include?(http_method.to_sym)
|
|
61
|
+
method(http_method).call(path, params, env)
|
|
62
|
+
else
|
|
63
|
+
env[:input] = json_body if json_body
|
|
64
|
+
method(http_method).call(path, nil, env)
|
|
65
|
+
end
|
|
56
66
|
end
|
|
57
67
|
end
|
|
58
68
|
|
data/lib/haveapi/version.rb
CHANGED