latitude-api 0.2.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/.yardopts +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/Rakefile +13 -0
- data/lib/latitude/api/api_object.rb +105 -0
- data/lib/latitude/api/api_resource.rb +260 -0
- data/lib/latitude/api/attributes.rb +64 -0
- data/lib/latitude/api/client.rb +80 -0
- data/lib/latitude/api/configuration.rb +88 -0
- data/lib/latitude/api/errors.rb +134 -0
- data/lib/latitude/api/json_api.rb +40 -0
- data/lib/latitude/api/list_object.rb +129 -0
- data/lib/latitude/api/objects.rb +216 -0
- data/lib/latitude/api/operations/action.rb +41 -0
- data/lib/latitude/api/operations/create.rb +22 -0
- data/lib/latitude/api/operations/delete.rb +22 -0
- data/lib/latitude/api/operations/list.rb +54 -0
- data/lib/latitude/api/operations/nested.rb +53 -0
- data/lib/latitude/api/operations/retrieve.rb +22 -0
- data/lib/latitude/api/operations/update.rb +22 -0
- data/lib/latitude/api/request_executor.rb +217 -0
- data/lib/latitude/api/request_options.rb +86 -0
- data/lib/latitude/api/request_params.rb +70 -0
- data/lib/latitude/api/resources/api_key.rb +61 -0
- data/lib/latitude/api/resources/billing_usage.rb +51 -0
- data/lib/latitude/api/resources/elastic_ip.rb +96 -0
- data/lib/latitude/api/resources/event.rb +55 -0
- data/lib/latitude/api/resources/firewall.rb +129 -0
- data/lib/latitude/api/resources/ip.rb +110 -0
- data/lib/latitude/api/resources/kubernetes_cluster.rb +184 -0
- data/lib/latitude/api/resources/plan.rb +658 -0
- data/lib/latitude/api/resources/project.rb +179 -0
- data/lib/latitude/api/resources/region.rb +41 -0
- data/lib/latitude/api/resources/role.rb +20 -0
- data/lib/latitude/api/resources/server.rb +251 -0
- data/lib/latitude/api/resources/ssh_key.rb +44 -0
- data/lib/latitude/api/resources/storage_filesystem.rb +36 -0
- data/lib/latitude/api/resources/storage_object.rb +85 -0
- data/lib/latitude/api/resources/storage_volume.rb +64 -0
- data/lib/latitude/api/resources/tag.rb +34 -0
- data/lib/latitude/api/resources/team.rb +169 -0
- data/lib/latitude/api/resources/traffic.rb +215 -0
- data/lib/latitude/api/resources/user_data.rb +38 -0
- data/lib/latitude/api/resources/user_profile.rb +51 -0
- data/lib/latitude/api/resources/user_team.rb +90 -0
- data/lib/latitude/api/resources/virtual_machine.rb +146 -0
- data/lib/latitude/api/resources/virtual_network.rb +162 -0
- data/lib/latitude/api/resources/vpn_session.rb +80 -0
- data/lib/latitude/api/singleton_api_resource.rb +22 -0
- data/lib/latitude/api/util.rb +64 -0
- data/lib/latitude/api/version.rb +7 -0
- data/lib/latitude/api.rb +147 -0
- data/lib/latitude/metadata/errors.rb +18 -0
- data/lib/latitude/metadata/session.rb +109 -0
- data/lib/latitude/metadata.rb +43 -0
- data/lib/latitude.rb +4 -0
- metadata +132 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Attributes
|
|
6
|
+
Spec = Struct.new(:name, :type, :read_only, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
# Attribute names that collide with core Ruby/internal methods. For these we
|
|
9
|
+
# still record the spec (so the value is wrapped and documented) but skip the
|
|
10
|
+
# generated accessor; read them via #[] instead.
|
|
11
|
+
RESERVED_NAMES = %w[method class send __send__ object_id __id__ hash dup clone
|
|
12
|
+
freeze frozen? instance_variable_get instance_variable_set
|
|
13
|
+
respond_to? is_a? kind_of? equal? itself].freeze
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
def attribute(name, type = :string, **opts)
|
|
17
|
+
name = name.to_s
|
|
18
|
+
read_only = opts.fetch(:read_only, false)
|
|
19
|
+
attribute_specs[name] = Spec.new(name: name, type: type, read_only: read_only)
|
|
20
|
+
|
|
21
|
+
unless RESERVED_NAMES.include?(name)
|
|
22
|
+
define_method(name) { read_attribute(name) }
|
|
23
|
+
define_method("#{name}=") { |value| write_attribute(name, value) } unless read_only
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attribute_specs[name]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attribute_specs
|
|
30
|
+
@attribute_specs ||= (superclass.respond_to?(:attribute_specs) ? superclass.attribute_specs : {}).dup
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
module InstanceMethods
|
|
35
|
+
def attribute_spec(name)
|
|
36
|
+
self.class.respond_to?(:attribute_specs) ? self.class.attribute_specs[name.to_s] : nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def read_attribute(name)
|
|
40
|
+
@values[name.to_s]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def wrap_attribute(key, value)
|
|
44
|
+
type = attribute_spec(key)&.type
|
|
45
|
+
|
|
46
|
+
if type.is_a?(Array)
|
|
47
|
+
klass = type.first
|
|
48
|
+
return Array(value).map { |v| v.is_a?(Hash) ? klass.new(v) : v } if object_class?(klass)
|
|
49
|
+
elsif object_class?(type)
|
|
50
|
+
return value.is_a?(Hash) ? type.new(value) : value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
APIObject.wrap(value)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def object_class?(type)
|
|
59
|
+
type.is_a?(Class) && type <= APIObject
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
class Client
|
|
5
|
+
RESOURCE_ACCESSORS = {
|
|
6
|
+
servers: "Latitude::API::Resources::Server",
|
|
7
|
+
regions: "Latitude::API::Resources::Region",
|
|
8
|
+
roles: "Latitude::API::Resources::Role",
|
|
9
|
+
events: "Latitude::API::Resources::Event",
|
|
10
|
+
ips: "Latitude::API::Resources::IP",
|
|
11
|
+
plans: "Latitude::API::Resources::Plan",
|
|
12
|
+
billing_usage: "Latitude::API::Resources::BillingUsage",
|
|
13
|
+
traffic: "Latitude::API::Resources::Traffic",
|
|
14
|
+
user_profile: "Latitude::API::Resources::UserProfile",
|
|
15
|
+
user_teams: "Latitude::API::Resources::UserTeam",
|
|
16
|
+
projects: "Latitude::API::Resources::Project",
|
|
17
|
+
ssh_keys: "Latitude::API::Resources::SSHKey",
|
|
18
|
+
user_data: "Latitude::API::Resources::UserData",
|
|
19
|
+
tags: "Latitude::API::Resources::Tag",
|
|
20
|
+
team: "Latitude::API::Resources::Team",
|
|
21
|
+
api_keys: "Latitude::API::Resources::APIKey",
|
|
22
|
+
elastic_ips: "Latitude::API::Resources::ElasticIP",
|
|
23
|
+
virtual_networks: "Latitude::API::Resources::VirtualNetwork",
|
|
24
|
+
firewalls: "Latitude::API::Resources::Firewall",
|
|
25
|
+
vpn_sessions: "Latitude::API::Resources::VPNSession",
|
|
26
|
+
storage_filesystems: "Latitude::API::Resources::Storage::Filesystem",
|
|
27
|
+
storage_volumes: "Latitude::API::Resources::Storage::Volume",
|
|
28
|
+
storage_objects: "Latitude::API::Resources::Storage::Object",
|
|
29
|
+
virtual_machines: "Latitude::API::Resources::VirtualMachine",
|
|
30
|
+
kubernetes_clusters: "Latitude::API::Resources::KubernetesCluster",
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
attr_reader :config
|
|
34
|
+
|
|
35
|
+
def initialize(**overrides)
|
|
36
|
+
@config = Latitude.config.dup_with(**overrides)
|
|
37
|
+
@accessors = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def configure
|
|
41
|
+
yield @config
|
|
42
|
+
@config
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def with_config(&blk)
|
|
46
|
+
Latitude::API::APIResource.with_config(@config, &blk)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
RESOURCE_ACCESSORS.each do |name, class_name|
|
|
50
|
+
define_method(name) do
|
|
51
|
+
@accessors[name] ||= ResourceFacade.new(class_name: class_name, config: @config)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ResourceFacade
|
|
56
|
+
def initialize(class_name:, config:)
|
|
57
|
+
@class_name = class_name
|
|
58
|
+
@config = config
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resource_class
|
|
62
|
+
@resource_class ||= ::Object.const_get(@class_name)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def method_missing(name, *args, **kwargs, &blk)
|
|
66
|
+
if resource_class.respond_to?(name)
|
|
67
|
+
Latitude::API::APIResource.with_config(@config) do
|
|
68
|
+
resource_class.public_send(name, *args, **kwargs, &blk)
|
|
69
|
+
end
|
|
70
|
+
else
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def respond_to_missing?(name, include_private = false)
|
|
76
|
+
resource_class.respond_to?(name) || super
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
class Configuration
|
|
6
|
+
DEFAULT_API_BASE = "https://api.latitude.sh"
|
|
7
|
+
DEFAULT_OPEN_TIMEOUT = 30
|
|
8
|
+
DEFAULT_READ_TIMEOUT = 80
|
|
9
|
+
DEFAULT_WRITE_TIMEOUT = 30
|
|
10
|
+
DEFAULT_MAX_NETWORK_RETRIES = 0
|
|
11
|
+
DEFAULT_INITIAL_NETWORK_RETRY_DELAY = 0.5
|
|
12
|
+
DEFAULT_MAX_NETWORK_RETRY_DELAY = 5.0
|
|
13
|
+
DEFAULT_SENSITIVE_FIELDS = %w[
|
|
14
|
+
authorization
|
|
15
|
+
public_key
|
|
16
|
+
private_key
|
|
17
|
+
password
|
|
18
|
+
api_key
|
|
19
|
+
token
|
|
20
|
+
kubeconfig
|
|
21
|
+
credentials
|
|
22
|
+
ipmi_password
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
attr_accessor :api_key,
|
|
26
|
+
:api_base,
|
|
27
|
+
:api_version,
|
|
28
|
+
:open_timeout,
|
|
29
|
+
:read_timeout,
|
|
30
|
+
:write_timeout,
|
|
31
|
+
:max_network_retries,
|
|
32
|
+
:initial_network_retry_delay,
|
|
33
|
+
:max_network_retry_delay,
|
|
34
|
+
:proxy,
|
|
35
|
+
:verify_ssl_certs,
|
|
36
|
+
:ca_bundle_path,
|
|
37
|
+
:ca_store,
|
|
38
|
+
:logger,
|
|
39
|
+
:log_level,
|
|
40
|
+
:app_info,
|
|
41
|
+
:enable_telemetry,
|
|
42
|
+
:sensitive_fields
|
|
43
|
+
|
|
44
|
+
def initialize
|
|
45
|
+
@api_key = nil
|
|
46
|
+
@api_base = DEFAULT_API_BASE
|
|
47
|
+
@api_version = nil
|
|
48
|
+
@open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
49
|
+
@read_timeout = DEFAULT_READ_TIMEOUT
|
|
50
|
+
@write_timeout = DEFAULT_WRITE_TIMEOUT
|
|
51
|
+
@max_network_retries = DEFAULT_MAX_NETWORK_RETRIES
|
|
52
|
+
@initial_network_retry_delay = DEFAULT_INITIAL_NETWORK_RETRY_DELAY
|
|
53
|
+
@max_network_retry_delay = DEFAULT_MAX_NETWORK_RETRY_DELAY
|
|
54
|
+
@proxy = nil
|
|
55
|
+
@verify_ssl_certs = true
|
|
56
|
+
@ca_bundle_path = nil
|
|
57
|
+
@ca_store = nil
|
|
58
|
+
@logger = nil
|
|
59
|
+
@log_level = :info
|
|
60
|
+
@app_info = nil
|
|
61
|
+
@enable_telemetry = true
|
|
62
|
+
@sensitive_fields = DEFAULT_SENSITIVE_FIELDS.dup
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dup_with(**overrides)
|
|
66
|
+
copy = Configuration.new
|
|
67
|
+
instance_variables.each do |ivar|
|
|
68
|
+
copy.instance_variable_set(ivar, instance_variable_get(ivar))
|
|
69
|
+
end
|
|
70
|
+
overrides.each { |k, v| copy.public_send("#{k}=", v) }
|
|
71
|
+
copy
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def api_base_uri
|
|
75
|
+
URI.parse(@api_base)
|
|
76
|
+
rescue URI::InvalidURIError => e
|
|
77
|
+
raise ConfigurationError, "api_base is not a valid URL: #{@api_base.inspect} (#{e.message})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_api_key!
|
|
81
|
+
return unless @api_key.nil? || @api_key.to_s.empty?
|
|
82
|
+
|
|
83
|
+
raise ConfigurationError,
|
|
84
|
+
"No API key provided. Set it via Latitude.api_key = '...' or pass api_key: in request options."
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ConfigurationError < Error; end
|
|
7
|
+
|
|
8
|
+
class ConnectionError < Error
|
|
9
|
+
attr_reader :cause_error
|
|
10
|
+
|
|
11
|
+
def initialize(message = nil, cause: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@cause_error = cause
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class APIError < Error
|
|
18
|
+
class Entry
|
|
19
|
+
attr_reader :status, :code, :title, :detail, :meta, :raw
|
|
20
|
+
|
|
21
|
+
def initialize(raw)
|
|
22
|
+
@raw = raw || {}
|
|
23
|
+
@status = @raw["status"]
|
|
24
|
+
@code = @raw["code"]
|
|
25
|
+
@title = @raw["title"]
|
|
26
|
+
@detail = @raw["detail"] || @raw["message"]
|
|
27
|
+
@meta = @raw["meta"] || {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
return @detail.to_s if @title.nil? || @title.empty?
|
|
32
|
+
return @title.to_s if @detail.nil? || @detail.empty?
|
|
33
|
+
|
|
34
|
+
"#{@title}: #{@detail}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
@raw.dup
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
attr_reader :http_status, :http_headers, :http_body, :json_body, :request_id, :entries
|
|
43
|
+
|
|
44
|
+
def initialize(message = nil, http_status: nil, http_headers: nil, http_body: nil, json_body: nil, request_id: nil)
|
|
45
|
+
@http_status = http_status
|
|
46
|
+
@http_headers = http_headers || {}
|
|
47
|
+
@http_body = http_body
|
|
48
|
+
@json_body = json_body
|
|
49
|
+
@request_id = request_id
|
|
50
|
+
@entries = Array(json_body.is_a?(Hash) ? json_body["errors"] : nil).map { |h| Entry.new(h) }
|
|
51
|
+
super(message || default_message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
alias errors entries
|
|
55
|
+
|
|
56
|
+
def code
|
|
57
|
+
entries.first&.code
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def title
|
|
61
|
+
entries.first&.title
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def detail
|
|
65
|
+
entries.first&.detail
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def default_message
|
|
71
|
+
first = entries.first
|
|
72
|
+
return first.to_s if first && !first.to_s.empty?
|
|
73
|
+
return "HTTP #{http_status}" if http_status
|
|
74
|
+
|
|
75
|
+
"API error"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class BadRequestError < APIError; end
|
|
80
|
+
class AuthenticationError < APIError; end
|
|
81
|
+
|
|
82
|
+
class PermissionError < APIError
|
|
83
|
+
READ_ONLY_CODES = %w[
|
|
84
|
+
READ_ONLY_API_KEY
|
|
85
|
+
READ_ONLY_KEY
|
|
86
|
+
read_only_api_key
|
|
87
|
+
read_only
|
|
88
|
+
].freeze
|
|
89
|
+
|
|
90
|
+
def read_only_key?
|
|
91
|
+
entries.any? { |e| READ_ONLY_CODES.include?(e.code.to_s) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def feature_not_enabled?
|
|
95
|
+
entries.any? { |e| e.code.to_s == "FEATURE_NOT_ENABLED" }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class NotFoundError < APIError; end
|
|
100
|
+
class ConflictError < APIError; end
|
|
101
|
+
class UnprocessableEntityError < APIError; end
|
|
102
|
+
|
|
103
|
+
class RateLimitError < APIError
|
|
104
|
+
def retry_after
|
|
105
|
+
meta_value = entries.map { |e| e.meta["retry_after"] }.compact.first
|
|
106
|
+
return Integer(meta_value) if meta_value
|
|
107
|
+
|
|
108
|
+
header_value = http_headers["retry-after"] || http_headers["Retry-After"]
|
|
109
|
+
return Integer(header_value) if header_value && header_value.to_s.match?(/\A\d+\z/)
|
|
110
|
+
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class ServerError < APIError; end
|
|
116
|
+
|
|
117
|
+
module API
|
|
118
|
+
class << self
|
|
119
|
+
def error_class_for_status(status)
|
|
120
|
+
case status
|
|
121
|
+
when 400 then BadRequestError
|
|
122
|
+
when 401 then AuthenticationError
|
|
123
|
+
when 403 then PermissionError
|
|
124
|
+
when 404 then NotFoundError
|
|
125
|
+
when 409 then ConflictError
|
|
126
|
+
when 422 then UnprocessableEntityError
|
|
127
|
+
when 429 then RateLimitError
|
|
128
|
+
when 500..599 then ServerError
|
|
129
|
+
else APIError
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module JSONAPI
|
|
6
|
+
MEDIA_TYPE = "application/vnd.api+json"
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def envelope(type:, attributes: nil, id: nil, relationships: nil)
|
|
11
|
+
data = { "type" => type.to_s }
|
|
12
|
+
data["id"] = id.to_s if id
|
|
13
|
+
data["attributes"] = Util.stringify_keys(attributes) if attributes
|
|
14
|
+
data["relationships"] = Util.stringify_keys(relationships) if relationships
|
|
15
|
+
{ "data" => data }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def encode(type:, attributes: nil, id: nil, relationships: nil)
|
|
19
|
+
JSON.generate(envelope(type: type, attributes: attributes, id: id, relationships: relationships))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse(body)
|
|
23
|
+
return nil if body.nil? || body.empty?
|
|
24
|
+
|
|
25
|
+
JSON.parse(body)
|
|
26
|
+
rescue JSON::ParserError => e
|
|
27
|
+
raise APIError.new(
|
|
28
|
+
"Invalid JSON response from server: #{e.message}",
|
|
29
|
+
http_body: body,
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extract_errors(parsed)
|
|
34
|
+
return [] unless parsed.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
Array(parsed["errors"])
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
class ListObject
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
attr_reader :data, :meta, :links, :request_params, :resource_class, :request_opts, :path_params
|
|
9
|
+
|
|
10
|
+
def initialize(resource_class:, parsed:, request_params: {}, request_opts: {}, path_params: {})
|
|
11
|
+
@resource_class = resource_class
|
|
12
|
+
@meta = parsed.is_a?(Hash) ? (parsed["meta"] || {}) : {}
|
|
13
|
+
@links = parsed.is_a?(Hash) ? (parsed["links"] || {}) : {}
|
|
14
|
+
raw_data = parsed.is_a?(Hash) ? Array(parsed["data"]) : []
|
|
15
|
+
@data = raw_data.map do |row|
|
|
16
|
+
resource_class.new(row, path_params: path_params, opts: request_opts)
|
|
17
|
+
end
|
|
18
|
+
@request_params = request_params || {}
|
|
19
|
+
@request_opts = request_opts || {}
|
|
20
|
+
@path_params = path_params || {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def each(&blk)
|
|
24
|
+
@data.each(&blk)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def size
|
|
28
|
+
@data.size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
alias length size
|
|
32
|
+
|
|
33
|
+
def empty?
|
|
34
|
+
@data.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def [](index)
|
|
38
|
+
@data[index]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def first
|
|
42
|
+
@data.first
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def last
|
|
46
|
+
@data.last
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_a
|
|
50
|
+
@data.dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def page_size
|
|
54
|
+
value = page_params["size"] || page_params[:size]
|
|
55
|
+
(value || 20).to_i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def page_number
|
|
59
|
+
value = page_params["number"] || page_params[:number]
|
|
60
|
+
(value || 1).to_i
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def has_more?
|
|
64
|
+
return !!@links["next"] if @links && @links["next"]
|
|
65
|
+
return page_number < @meta["total_pages"].to_i if @meta && @meta["total_pages"]
|
|
66
|
+
|
|
67
|
+
@data.size >= page_size
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def next_page(opts = {})
|
|
71
|
+
return empty_page unless has_more?
|
|
72
|
+
|
|
73
|
+
params = deep_dup(@request_params)
|
|
74
|
+
params[:page] ||= {}
|
|
75
|
+
params[:page][:number] = page_number + 1
|
|
76
|
+
params[:page][:size] = page_size
|
|
77
|
+
fetch(params, opts)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def previous_page(opts = {})
|
|
81
|
+
return empty_page if page_number <= 1
|
|
82
|
+
|
|
83
|
+
params = deep_dup(@request_params)
|
|
84
|
+
params[:page] ||= {}
|
|
85
|
+
params[:page][:number] = page_number - 1
|
|
86
|
+
params[:page][:size] = page_size
|
|
87
|
+
fetch(params, opts)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def auto_paging_each(&blk)
|
|
91
|
+
page = self
|
|
92
|
+
loop do
|
|
93
|
+
page.each(&blk)
|
|
94
|
+
break unless page.has_more?
|
|
95
|
+
|
|
96
|
+
page = page.next_page
|
|
97
|
+
end
|
|
98
|
+
self
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def page_params
|
|
104
|
+
(@request_params[:page] || @request_params["page"] || {})
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def fetch(params, opts)
|
|
108
|
+
merged_opts = @request_opts.merge(opts || {})
|
|
109
|
+
params = params.merge(_path_params: @path_params)
|
|
110
|
+
@resource_class.list(params, merged_opts)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def empty_page
|
|
114
|
+
self.class.new(resource_class: @resource_class, parsed: { "data" => [] },
|
|
115
|
+
request_params: @request_params, request_opts: @request_opts, path_params: @path_params)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def deep_dup(hash)
|
|
119
|
+
hash.each_with_object({}) do |(k, v), out|
|
|
120
|
+
out[k] = case v
|
|
121
|
+
when Hash then deep_dup(v)
|
|
122
|
+
when Array then v.dup
|
|
123
|
+
else v
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|