cased-ruby 0.3.3
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/.github/workflows/rubocop.yml +21 -0
- data/.github/workflows/ruby.yml +46 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +88 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +107 -0
- data/LICENSE +21 -0
- data/README.md +661 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/rubocop +29 -0
- data/cased-ruby.gemspec +48 -0
- data/lib/cased-ruby.rb +3 -0
- data/lib/cased.rb +260 -0
- data/lib/cased/clients.rb +21 -0
- data/lib/cased/collection_response.rb +117 -0
- data/lib/cased/config.rb +172 -0
- data/lib/cased/context.rb +50 -0
- data/lib/cased/context/expander.rb +33 -0
- data/lib/cased/error.rb +8 -0
- data/lib/cased/http/client.rb +83 -0
- data/lib/cased/http/error.rb +99 -0
- data/lib/cased/instrumentation/controller.rb +34 -0
- data/lib/cased/instrumentation/log_subscriber.rb +31 -0
- data/lib/cased/integrations/sidekiq.rb +17 -0
- data/lib/cased/integrations/sidekiq/client_middleware.rb +14 -0
- data/lib/cased/integrations/sidekiq/server_middleware.rb +20 -0
- data/lib/cased/model.rb +98 -0
- data/lib/cased/policy.rb +24 -0
- data/lib/cased/publishers.rb +6 -0
- data/lib/cased/publishers/active_support_publisher.rb +16 -0
- data/lib/cased/publishers/base.rb +17 -0
- data/lib/cased/publishers/error.rb +11 -0
- data/lib/cased/publishers/http_publisher.rb +15 -0
- data/lib/cased/publishers/null_publisher.rb +11 -0
- data/lib/cased/publishers/test_publisher.rb +19 -0
- data/lib/cased/query.rb +87 -0
- data/lib/cased/rack_middleware.rb +15 -0
- data/lib/cased/response.rb +37 -0
- data/lib/cased/sensitive.rb +4 -0
- data/lib/cased/sensitive/handler.rb +54 -0
- data/lib/cased/sensitive/processor.rb +78 -0
- data/lib/cased/sensitive/range.rb +54 -0
- data/lib/cased/sensitive/result.rb +8 -0
- data/lib/cased/sensitive/string.rb +43 -0
- data/lib/cased/test_helper.rb +188 -0
- data/lib/cased/version.rb +5 -0
- data/vendor/cache/activesupport-6.0.3.4.gem +0 -0
- data/vendor/cache/addressable-2.7.0.gem +0 -0
- data/vendor/cache/ast-2.4.0.gem +0 -0
- data/vendor/cache/byebug-11.0.1.gem +0 -0
- data/vendor/cache/concurrent-ruby-1.1.7.gem +0 -0
- data/vendor/cache/connection_pool-2.2.2.gem +0 -0
- data/vendor/cache/crack-0.4.3.gem +0 -0
- data/vendor/cache/docile-1.3.2.gem +0 -0
- data/vendor/cache/dotpath-0.1.0.gem +0 -0
- data/vendor/cache/faraday-1.1.0.gem +0 -0
- data/vendor/cache/faraday_middleware-1.0.0.gem +0 -0
- data/vendor/cache/hashdiff-1.0.1.gem +0 -0
- data/vendor/cache/i18n-1.8.5.gem +0 -0
- data/vendor/cache/jaro_winkler-1.5.4.gem +0 -0
- data/vendor/cache/json-2.3.1.gem +0 -0
- data/vendor/cache/minitest-5.13.0.gem +0 -0
- data/vendor/cache/mocha-1.11.2.gem +0 -0
- data/vendor/cache/multipart-post-2.1.1.gem +0 -0
- data/vendor/cache/net-http-persistent-3.1.0.gem +0 -0
- data/vendor/cache/parallel-1.19.1.gem +0 -0
- data/vendor/cache/parser-2.7.1.3.gem +0 -0
- data/vendor/cache/public_suffix-4.0.5.gem +0 -0
- data/vendor/cache/rack-2.2.2.gem +0 -0
- data/vendor/cache/rack-protection-2.0.8.1.gem +0 -0
- data/vendor/cache/rainbow-3.0.0.gem +0 -0
- data/vendor/cache/rake-10.5.0.gem +0 -0
- data/vendor/cache/redis-4.1.4.gem +0 -0
- data/vendor/cache/rubocop-0.78.0.gem +0 -0
- data/vendor/cache/rubocop-performance-1.5.2.gem +0 -0
- data/vendor/cache/ruby-progressbar-1.10.1.gem +0 -0
- data/vendor/cache/ruby2_keywords-0.0.2.gem +0 -0
- data/vendor/cache/safe_yaml-1.0.5.gem +0 -0
- data/vendor/cache/sidekiq-6.0.7.gem +0 -0
- data/vendor/cache/simplecov-0.18.5.gem +0 -0
- data/vendor/cache/simplecov-html-0.12.2.gem +0 -0
- data/vendor/cache/thread_safe-0.3.6.gem +0 -0
- data/vendor/cache/tzinfo-1.2.7.gem +0 -0
- data/vendor/cache/unicode-display_width-1.6.1.gem +0 -0
- data/vendor/cache/webmock-3.8.3.gem +0 -0
- data/vendor/cache/yard-0.9.24.gem +0 -0
- data/vendor/cache/zeitwerk-2.4.0.gem +0 -0
- metadata +375 -0
data/Rakefile
ADDED
data/bin/console
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'cased-ruby'
|
|
6
|
+
|
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
9
|
+
|
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
11
|
+
# require "pry"
|
|
12
|
+
# Pry.start
|
|
13
|
+
|
|
14
|
+
require 'irb'
|
|
15
|
+
IRB.start(__FILE__)
|
data/bin/rubocop
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
#
|
|
5
|
+
# This file was generated by Bundler.
|
|
6
|
+
#
|
|
7
|
+
# The application 'rubocop' is installed as part of a gem, and
|
|
8
|
+
# this file is here to facilitate running it.
|
|
9
|
+
#
|
|
10
|
+
|
|
11
|
+
require 'pathname'
|
|
12
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
|
|
13
|
+
Pathname.new(__FILE__).realpath)
|
|
14
|
+
|
|
15
|
+
bundle_binstub = File.expand_path('bundle', __dir__)
|
|
16
|
+
|
|
17
|
+
if File.file?(bundle_binstub)
|
|
18
|
+
if /This file was generated by Bundler/.match?(File.read(bundle_binstub, 300))
|
|
19
|
+
load(bundle_binstub)
|
|
20
|
+
else
|
|
21
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
|
22
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require 'rubygems'
|
|
27
|
+
require 'bundler/setup'
|
|
28
|
+
|
|
29
|
+
load Gem.bin_path('rubocop', 'rubocop')
|
data/cased-ruby.gemspec
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
|
+
require 'cased/version'
|
|
6
|
+
|
|
7
|
+
Gem::Specification.new do |spec|
|
|
8
|
+
spec.name = 'cased-ruby'
|
|
9
|
+
spec.version = Cased::VERSION
|
|
10
|
+
spec.authors = ['Garrett Bjerkhoel']
|
|
11
|
+
spec.email = ['garrett@cased.com']
|
|
12
|
+
|
|
13
|
+
spec.summary = 'Ruby library for Cased'
|
|
14
|
+
spec.description = 'Ruby library for Cased'
|
|
15
|
+
spec.homepage = 'https://github.com/cased/cased-ruby'
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/cased/cased-ruby'
|
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/cased/cased-ruby/releases'
|
|
21
|
+
|
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
24
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
25
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = 'exe'
|
|
28
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
|
|
31
|
+
spec.add_dependency 'activesupport', '~> 6'
|
|
32
|
+
spec.add_dependency 'dotpath', '0.1.0'
|
|
33
|
+
spec.add_dependency 'faraday', '~> 1.0'
|
|
34
|
+
spec.add_dependency 'faraday_middleware', '~> 1.0'
|
|
35
|
+
spec.add_dependency 'json', '~> 2'
|
|
36
|
+
spec.add_dependency 'net-http-persistent', '~> 3.0'
|
|
37
|
+
spec.add_development_dependency 'bundler', '2.1.4'
|
|
38
|
+
spec.add_development_dependency 'byebug', '11.0.1'
|
|
39
|
+
spec.add_development_dependency 'minitest', '5.13.0'
|
|
40
|
+
spec.add_development_dependency 'mocha', '1.11.2'
|
|
41
|
+
spec.add_development_dependency 'rack', '2.2.2'
|
|
42
|
+
spec.add_development_dependency 'rake', '10.5.0'
|
|
43
|
+
spec.add_development_dependency 'rubocop', '0.78.0'
|
|
44
|
+
spec.add_development_dependency 'rubocop-performance', '1.5.2'
|
|
45
|
+
spec.add_development_dependency 'sidekiq', '6.0.7'
|
|
46
|
+
spec.add_development_dependency 'webmock', '3.8.3'
|
|
47
|
+
spec.add_development_dependency 'yard', '0.9.24'
|
|
48
|
+
end
|
data/lib/cased-ruby.rb
ADDED
data/lib/cased.rb
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'dotpath'
|
|
6
|
+
require 'cased/version'
|
|
7
|
+
require 'cased/config'
|
|
8
|
+
require 'cased/context'
|
|
9
|
+
require 'cased/model'
|
|
10
|
+
require 'cased/error'
|
|
11
|
+
require 'cased/clients'
|
|
12
|
+
require 'cased/policy'
|
|
13
|
+
require 'cased/rack_middleware'
|
|
14
|
+
require 'cased/sensitive'
|
|
15
|
+
require 'cased/publishers'
|
|
16
|
+
require 'cased/test_helper'
|
|
17
|
+
require 'cased/instrumentation/log_subscriber'
|
|
18
|
+
|
|
19
|
+
# Integrations
|
|
20
|
+
begin
|
|
21
|
+
require 'cased/integrations/sidekiq'
|
|
22
|
+
rescue LoadError # rubocop:disable Lint/SuppressedException
|
|
23
|
+
# Sidekiq is not installed in host application
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Cased
|
|
27
|
+
def self.sensitive(label, handler)
|
|
28
|
+
Cased::Sensitive::Handler.register(label, handler)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# Cased.policies[:organization]
|
|
34
|
+
#
|
|
35
|
+
# @return [Hash{Symbol => Cased::Policy, nil}]
|
|
36
|
+
def self.policies
|
|
37
|
+
@policies ||= Hash.new do |hash, name|
|
|
38
|
+
key = name.to_sym
|
|
39
|
+
api_key = Cased.config.policy_key(key)
|
|
40
|
+
hash[key] = Policy.new(api_key: api_key)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Helper method for accessing the applications default policy.
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# Cased.configure do |config|
|
|
48
|
+
# config.policy_key = 'policy_test_1dQpY5JliYgHSkEntAbMVzuOROh'
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# policy = Cased.policy
|
|
52
|
+
# policy.events.each do |event|
|
|
53
|
+
# puts event['action'] # => user.login
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# @return [Cased::Policy, nil]
|
|
57
|
+
def self.policy
|
|
58
|
+
policies[:default]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @return [Cased::Config]
|
|
62
|
+
def self.config
|
|
63
|
+
@config ||= Cased::Config.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @example
|
|
67
|
+
# Cased.configure do |config|
|
|
68
|
+
# config.policy_key = 'policy_test_1dQpY5JliYgHSkEntAbMVzuOROh'
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# @return [void]
|
|
72
|
+
def self.configure(&block)
|
|
73
|
+
block.call(config)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class << self
|
|
77
|
+
attr_writer :publishers
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The list of publishers that will receive the processed audit event when calling Cased.publish.
|
|
81
|
+
#
|
|
82
|
+
# The desired behavior for Cased.publish should not change based on the order
|
|
83
|
+
# of the publishers.
|
|
84
|
+
#
|
|
85
|
+
# @example Adding a publisher to the stack
|
|
86
|
+
# Cased.publishers << Cased::Publishers::KafkaPublisher.new
|
|
87
|
+
#
|
|
88
|
+
# @example Setting the list of publishers
|
|
89
|
+
# Cased.publishers = [
|
|
90
|
+
# Cased::Publishers::KafkaPublisher.new,
|
|
91
|
+
# ]
|
|
92
|
+
#
|
|
93
|
+
# @return [Array<Cased::Publishers::Base>]
|
|
94
|
+
def self.publishers
|
|
95
|
+
@publishers ||= [
|
|
96
|
+
Cased::Publishers::HTTPPublisher.new,
|
|
97
|
+
Cased::Publishers::ActiveSupportPublisher.new,
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.clients
|
|
102
|
+
@clients ||= Cased::Clients.new
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# @param audit_event [Hash] the audit event.
|
|
106
|
+
#
|
|
107
|
+
# @example
|
|
108
|
+
# Cased.publish(
|
|
109
|
+
# action: "user.login",
|
|
110
|
+
# actor: "garrett@cased.com",
|
|
111
|
+
# actor_id: "user_1dQpY5JliYgHSkEntAbMVzuOROh",
|
|
112
|
+
# http_user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36",
|
|
113
|
+
# http_url: "https://app.cased.com/",
|
|
114
|
+
# http_method: "GET",
|
|
115
|
+
# language: "en-US"
|
|
116
|
+
# )
|
|
117
|
+
#
|
|
118
|
+
# @example With User object that includes Cased::Model
|
|
119
|
+
# Cased.publish(
|
|
120
|
+
# action: "user.login",
|
|
121
|
+
# actor: User.find(1),
|
|
122
|
+
# )
|
|
123
|
+
#
|
|
124
|
+
# @return [Array] of responses from Cased.publishers
|
|
125
|
+
# @return [false] if Cased has been silenced.
|
|
126
|
+
# @raise [ArgumentError] if a publisher does not implement the #publish method
|
|
127
|
+
def self.publish(audit_event)
|
|
128
|
+
return false if config.silence?
|
|
129
|
+
|
|
130
|
+
processed_audit_event = process(audit_event)
|
|
131
|
+
|
|
132
|
+
publishers.each do |publisher|
|
|
133
|
+
unless publisher.respond_to?(:publish)
|
|
134
|
+
raise ArgumentError, "#{publisher.class} must implement #{publisher.class}#publish"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
publisher.publish(processed_audit_event.dup)
|
|
138
|
+
rescue StandardError => e
|
|
139
|
+
handle_exception(e)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [Cased::Context]
|
|
144
|
+
def self.context
|
|
145
|
+
Context.current
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The main entry point to handling any exceptions encountered in the event creation lifecycle.
|
|
149
|
+
#
|
|
150
|
+
# @param exception [Exception] the exception to be raised
|
|
151
|
+
#
|
|
152
|
+
# @return [void]
|
|
153
|
+
# @raise [Exception] if Cased is configured to raise on errors
|
|
154
|
+
def self.handle_exception(exception)
|
|
155
|
+
raise exception if config.raise_on_errors?
|
|
156
|
+
|
|
157
|
+
if exception_handler.nil?
|
|
158
|
+
warn exception.message
|
|
159
|
+
return
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
exception_handler.call(exception)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Applications can determine where they want exceptions generated by Cased to be sent.
|
|
166
|
+
#
|
|
167
|
+
# @return [Proc, nil]
|
|
168
|
+
def self.exception_handler
|
|
169
|
+
@exception_handler
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Sets the system user to be used for Cased events that do not contain an actor.
|
|
173
|
+
#
|
|
174
|
+
# @param handler [Proc] - The Proc or lambda that takes a single exception argument.
|
|
175
|
+
#
|
|
176
|
+
# @example
|
|
177
|
+
# Cased.exception_handler = Proc.new do |exception|
|
|
178
|
+
# Raven.capture_exception(exception)
|
|
179
|
+
# end
|
|
180
|
+
#
|
|
181
|
+
# @return [void]
|
|
182
|
+
# @raise [ArgumentError] if the provided handler does not respond to call or
|
|
183
|
+
# accept an argument.
|
|
184
|
+
def self.exception_handler=(handler)
|
|
185
|
+
if handler.nil?
|
|
186
|
+
@exception_handler = nil
|
|
187
|
+
return
|
|
188
|
+
elsif !handler.respond_to?(:call)
|
|
189
|
+
@exception_handler = nil
|
|
190
|
+
raise ArgumentError, "#{handler.class} does not respond to #call"
|
|
191
|
+
elsif handler.arity != 1
|
|
192
|
+
raise ArgumentError, 'handler does not accept any arguments'
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
@exception_handler = handler
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Configures a default context for console sessions.
|
|
199
|
+
#
|
|
200
|
+
# When a console session is started you don't have the context generated from
|
|
201
|
+
# a typical web request lifecycle where authentication will happen and an IP
|
|
202
|
+
# address is present. This uses the server's hostname as a standard location
|
|
203
|
+
# for migrations or data transitions.
|
|
204
|
+
#
|
|
205
|
+
# @param options [Hash]
|
|
206
|
+
#
|
|
207
|
+
# @return [void]
|
|
208
|
+
def self.console(options = {})
|
|
209
|
+
context.merge({
|
|
210
|
+
location: Socket.gethostname,
|
|
211
|
+
}.merge(options))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Generates Cased compatible resource identifier.
|
|
215
|
+
#
|
|
216
|
+
# @param model [Object] an object that responds to #cased_id
|
|
217
|
+
#
|
|
218
|
+
# @return [String] the Cased::Model#cased_id
|
|
219
|
+
# @raise [Cased::Error::MissingIdentifier] when the provided model does not
|
|
220
|
+
# respond to #cased_id
|
|
221
|
+
def self.id(model)
|
|
222
|
+
raise Cased::Error::MissingIdentifier unless model.respond_to?(:cased_id)
|
|
223
|
+
|
|
224
|
+
model.cased_id
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Don't send any events to Cased that are created within the lifecycle of the block.
|
|
228
|
+
#
|
|
229
|
+
# @example
|
|
230
|
+
# Cased.silence do
|
|
231
|
+
# user.save
|
|
232
|
+
# end
|
|
233
|
+
def self.silence
|
|
234
|
+
original_silence = config.silence?
|
|
235
|
+
config.silence = true
|
|
236
|
+
yield
|
|
237
|
+
ensure
|
|
238
|
+
config.silence = original_silence
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Publish the Cased event so all subscribers can handle storing the Cased event.
|
|
242
|
+
#
|
|
243
|
+
# @param payload [Hash] payload to publish.
|
|
244
|
+
#
|
|
245
|
+
# @return [Hash] the processed audit event payload.
|
|
246
|
+
private_class_method def self.process(payload)
|
|
247
|
+
expanded_audit_event = Cased::Context::Expander.expand(payload)
|
|
248
|
+
event = expanded_audit_event.merge(
|
|
249
|
+
cased_id: SecureRandom.hex,
|
|
250
|
+
timestamp: Time.now.utc.iso8601(6),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
safe_context = context.context.dup
|
|
254
|
+
audit_event = safe_context.merge(event)
|
|
255
|
+
|
|
256
|
+
Cased::Sensitive::Processor.process!(audit_event)
|
|
257
|
+
|
|
258
|
+
audit_event
|
|
259
|
+
end
|
|
260
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cased/http/client'
|
|
4
|
+
|
|
5
|
+
module Cased
|
|
6
|
+
class Clients
|
|
7
|
+
def self.create(api_key:, url: nil)
|
|
8
|
+
url ||= Cased.config.api_url
|
|
9
|
+
|
|
10
|
+
Cased::HTTP::Client.new(url: url, api_key: api_key)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def organization
|
|
14
|
+
@organization ||= self.class.create(api_key: ENV.fetch('CASED_ORGANIZATION_KEY'))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def publish
|
|
18
|
+
@publish ||= self.class.create(url: Cased.config.publish_url, api_key: Cased.config.publish_key)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'cased/response'
|
|
4
|
+
|
|
5
|
+
module Cased
|
|
6
|
+
class CollectionResponse < Response
|
|
7
|
+
def results
|
|
8
|
+
return [] unless body
|
|
9
|
+
|
|
10
|
+
body['results']
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def total_count
|
|
14
|
+
return unless body
|
|
15
|
+
|
|
16
|
+
body['total_count']
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def total_pages
|
|
20
|
+
return unless body
|
|
21
|
+
|
|
22
|
+
body['total_pages']
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def next_page_url?
|
|
26
|
+
next_page_url.present?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def next_page_url
|
|
30
|
+
links[:next]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def next_page
|
|
34
|
+
page_from(:next)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def next_page?
|
|
38
|
+
next_page.present?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def previous_page_url?
|
|
42
|
+
previous_page_url.present?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def previous_page_url
|
|
46
|
+
links[:prev]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def previous_page
|
|
50
|
+
page_from(:prev)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def previous_page?
|
|
54
|
+
previous_page.present?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def first_page_url?
|
|
58
|
+
first_page_url.present?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def first_page_url
|
|
62
|
+
links[:first]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def first_page
|
|
66
|
+
page_from(:first)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def first_page?
|
|
70
|
+
first_page.present?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def last_page_url?
|
|
74
|
+
last_page_url.present?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def last_page_url
|
|
78
|
+
links[:last]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def last_page
|
|
82
|
+
page_from(:last)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def last_page?
|
|
86
|
+
last_page.present?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def page_from(rel)
|
|
92
|
+
rel = links[rel.to_sym]
|
|
93
|
+
return unless rel
|
|
94
|
+
|
|
95
|
+
uri = Addressable::URI.parse(rel)
|
|
96
|
+
return unless uri
|
|
97
|
+
|
|
98
|
+
page = uri.query_values['page']
|
|
99
|
+
return unless page
|
|
100
|
+
|
|
101
|
+
page.to_i
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def links
|
|
105
|
+
link_header = @response.headers['Link']
|
|
106
|
+
return {} unless link_header
|
|
107
|
+
|
|
108
|
+
links = link_header.split(', ').map do |link|
|
|
109
|
+
href, name = link.match(/<(.*?)>; rel="(\w+)"/).captures
|
|
110
|
+
|
|
111
|
+
[name.to_sym, href]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Hash[*links.flatten]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|