petstore_api_client 0.1.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/.editorconfig +33 -0
- data/.env.example +50 -0
- data/.github/CODEOWNERS +36 -0
- data/.github/workflows/ci.yml +157 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTORS.md +39 -0
- data/LICENSE +21 -0
- data/README.md +684 -0
- data/Rakefile +12 -0
- data/lib/petstore_api_client/api_client.rb +60 -0
- data/lib/petstore_api_client/authentication/api_key.rb +107 -0
- data/lib/petstore_api_client/authentication/base.rb +113 -0
- data/lib/petstore_api_client/authentication/composite.rb +178 -0
- data/lib/petstore_api_client/authentication/none.rb +42 -0
- data/lib/petstore_api_client/authentication/oauth2.rb +305 -0
- data/lib/petstore_api_client/client.rb +87 -0
- data/lib/petstore_api_client/clients/concerns/pagination.rb +124 -0
- data/lib/petstore_api_client/clients/concerns/resource_operations.rb +121 -0
- data/lib/petstore_api_client/clients/pet_client.rb +119 -0
- data/lib/petstore_api_client/clients/store_client.rb +37 -0
- data/lib/petstore_api_client/configuration.rb +318 -0
- data/lib/petstore_api_client/connection.rb +55 -0
- data/lib/petstore_api_client/errors.rb +70 -0
- data/lib/petstore_api_client/middleware/authentication.rb +44 -0
- data/lib/petstore_api_client/models/api_response.rb +31 -0
- data/lib/petstore_api_client/models/base.rb +60 -0
- data/lib/petstore_api_client/models/category.rb +17 -0
- data/lib/petstore_api_client/models/named_entity.rb +36 -0
- data/lib/petstore_api_client/models/order.rb +55 -0
- data/lib/petstore_api_client/models/pet.rb +225 -0
- data/lib/petstore_api_client/models/tag.rb +20 -0
- data/lib/petstore_api_client/paginated_collection.rb +133 -0
- data/lib/petstore_api_client/request.rb +225 -0
- data/lib/petstore_api_client/response.rb +193 -0
- data/lib/petstore_api_client/validators/array_presence_validator.rb +15 -0
- data/lib/petstore_api_client/validators/enum_validator.rb +17 -0
- data/lib/petstore_api_client/version.rb +5 -0
- data/lib/petstore_api_client.rb +55 -0
- metadata +252 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetstoreApiClient
|
|
4
|
+
module Models
|
|
5
|
+
# Base class for all API models
|
|
6
|
+
# Provides common ActiveModel functionality and shared behavior
|
|
7
|
+
#
|
|
8
|
+
# This follows the DRY principle by centralizing all the common
|
|
9
|
+
# ActiveModel includes that every model needs.
|
|
10
|
+
#
|
|
11
|
+
# Benefits:
|
|
12
|
+
# - Single place to add model-wide functionality
|
|
13
|
+
# - Consistent behavior across all models
|
|
14
|
+
# - Reduces boilerplate in model classes
|
|
15
|
+
class Base
|
|
16
|
+
include ActiveModel::Model
|
|
17
|
+
include ActiveModel::Attributes
|
|
18
|
+
include ActiveModel::Validations
|
|
19
|
+
include ActiveModel::Serialization
|
|
20
|
+
|
|
21
|
+
# Convert model to hash for API requests
|
|
22
|
+
# Subclasses can override this for custom serialization
|
|
23
|
+
# Default implementation uses compact to remove nil values
|
|
24
|
+
def to_h
|
|
25
|
+
attributes.compact
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create model instance from API response data
|
|
29
|
+
# This is a template method - subclasses must implement their own
|
|
30
|
+
# field mapping logic since each model has different fields
|
|
31
|
+
#
|
|
32
|
+
# @param data [Hash] Response data from API
|
|
33
|
+
# @return [Base, nil] Model instance or nil if data is nil
|
|
34
|
+
def self.from_response(data)
|
|
35
|
+
raise NotImplementedError, "#{name} must implement .from_response"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
protected
|
|
40
|
+
|
|
41
|
+
# Helper method to extract value from response data
|
|
42
|
+
# Tries multiple key formats: camelCase, snake_case, symbol
|
|
43
|
+
#
|
|
44
|
+
# @param data [Hash] Response data
|
|
45
|
+
# @param field [Symbol] Field name in snake_case
|
|
46
|
+
# @param api_key [String, nil] Optional camelCase API key (if different from field)
|
|
47
|
+
# @return [Object, nil] Field value or nil
|
|
48
|
+
def extract_field(data, field, api_key = nil)
|
|
49
|
+
return nil if data.nil?
|
|
50
|
+
|
|
51
|
+
# Try API key (camelCase) first if provided
|
|
52
|
+
value = data[api_key] if api_key
|
|
53
|
+
# Then try snake_case string and symbol
|
|
54
|
+
value ||= data[field.to_s] || data[field]
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "named_entity"
|
|
4
|
+
|
|
5
|
+
module PetstoreApiClient
|
|
6
|
+
module Models
|
|
7
|
+
# Category model - represents a pet category
|
|
8
|
+
# Inherits all behavior from NamedEntity (id + name)
|
|
9
|
+
#
|
|
10
|
+
# Refactored to eliminate 100% duplication with Tag model
|
|
11
|
+
# by inheriting from shared NamedEntity base class
|
|
12
|
+
class Category < NamedEntity
|
|
13
|
+
# Category-specific logic can be added here if needed
|
|
14
|
+
# For now, it just uses the base NamedEntity behavior
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module PetstoreApiClient
|
|
6
|
+
module Models
|
|
7
|
+
# Base class for simple entities with just ID and name
|
|
8
|
+
# Used by Category and Tag models to eliminate duplication
|
|
9
|
+
#
|
|
10
|
+
# This is an example of the DRY principle - both Category and Tag
|
|
11
|
+
# were 100% identical, so we extracted their common behavior here.
|
|
12
|
+
class NamedEntity < Base
|
|
13
|
+
attribute :id, :integer
|
|
14
|
+
attribute :name, :string
|
|
15
|
+
|
|
16
|
+
# Override to_h to use symbol keys (API expects this format)
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
id: id,
|
|
20
|
+
name: name
|
|
21
|
+
}.compact
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Create from API response data
|
|
25
|
+
# Handles both string and symbol keys
|
|
26
|
+
def self.from_response(data)
|
|
27
|
+
return nil if data.nil?
|
|
28
|
+
|
|
29
|
+
new(
|
|
30
|
+
id: extract_field(data, :id),
|
|
31
|
+
name: extract_field(data, :name)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetstoreApiClient
|
|
4
|
+
module Models
|
|
5
|
+
# Order model representing a store order
|
|
6
|
+
# TODO: Should validate quantity > 0 and pet_id presence, but assignment doc didn't specify
|
|
7
|
+
class Order < Base
|
|
8
|
+
VALID_STATUSES = %w[placed approved delivered].freeze
|
|
9
|
+
|
|
10
|
+
attribute :id, :integer
|
|
11
|
+
attribute :pet_id, :integer
|
|
12
|
+
attribute :quantity, :integer
|
|
13
|
+
attribute :ship_date, :datetime
|
|
14
|
+
attribute :status, :string
|
|
15
|
+
attribute :complete, :boolean, default: false
|
|
16
|
+
|
|
17
|
+
validates :status, enum: { in: VALID_STATUSES, allow_nil: true }, if: -> { status.present? }
|
|
18
|
+
|
|
19
|
+
# Note: API expects snake_case to be converted to camelCase
|
|
20
|
+
def to_h
|
|
21
|
+
{
|
|
22
|
+
id: id,
|
|
23
|
+
petId: pet_id,
|
|
24
|
+
quantity: quantity,
|
|
25
|
+
shipDate: ship_date&.iso8601,
|
|
26
|
+
status: status,
|
|
27
|
+
complete: complete
|
|
28
|
+
}.compact
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.from_response(data)
|
|
32
|
+
return nil if data.nil?
|
|
33
|
+
|
|
34
|
+
# Parse ship_date if it's a string
|
|
35
|
+
ship_date = data["shipDate"] || data[:shipDate] || data[:ship_date]
|
|
36
|
+
ship_date = parse_datetime(ship_date) if ship_date.is_a?(String)
|
|
37
|
+
|
|
38
|
+
new(
|
|
39
|
+
id: data["id"] || data[:id],
|
|
40
|
+
pet_id: data["petId"] || data[:petId] || data[:pet_id],
|
|
41
|
+
quantity: data["quantity"] || data[:quantity],
|
|
42
|
+
ship_date: ship_date,
|
|
43
|
+
status: data["status"] || data[:status],
|
|
44
|
+
complete: data["complete"] || data[:complete] || false
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.parse_datetime(datetime_string)
|
|
49
|
+
DateTime.parse(datetime_string)
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
nil # Silently fail - API shouldn't send invalid dates anyway
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetstoreApiClient
|
|
4
|
+
module Models
|
|
5
|
+
# Pet resource model
|
|
6
|
+
#
|
|
7
|
+
# Represents a pet in the Petstore API with comprehensive validations
|
|
8
|
+
# following the API specification. Uses ActiveModel for validation,
|
|
9
|
+
# attribute management, and serialization.
|
|
10
|
+
#
|
|
11
|
+
# The Pet model supports:
|
|
12
|
+
# - Required and optional attributes with type casting
|
|
13
|
+
# - Nested objects (Category, Tags)
|
|
14
|
+
# - Status enum validation
|
|
15
|
+
# - Bi-directional conversion (to API format and from API responses)
|
|
16
|
+
# - ActiveModel validations
|
|
17
|
+
#
|
|
18
|
+
# @example Creating a new pet
|
|
19
|
+
# pet = Pet.new(
|
|
20
|
+
# name: "Fluffy",
|
|
21
|
+
# status: "available",
|
|
22
|
+
# photo_urls: ["https://example.com/photo1.jpg"]
|
|
23
|
+
# )
|
|
24
|
+
#
|
|
25
|
+
# @example Creating a pet with nested category
|
|
26
|
+
# pet = Pet.new(
|
|
27
|
+
# name: "Fluffy",
|
|
28
|
+
# status: "available",
|
|
29
|
+
# category: { id: 1, name: "Dogs" },
|
|
30
|
+
# tags: [{ id: 1, name: "friendly" }],
|
|
31
|
+
# photo_urls: ["https://example.com/photo1.jpg"]
|
|
32
|
+
# )
|
|
33
|
+
#
|
|
34
|
+
# @example Creating from API response
|
|
35
|
+
# data = { "id" => 123, "name" => "Fluffy", "status" => "available" }
|
|
36
|
+
# pet = Pet.from_response(data)
|
|
37
|
+
#
|
|
38
|
+
# @example Converting to API format
|
|
39
|
+
# pet_hash = pet.to_h
|
|
40
|
+
# # Returns: { id: 123, name: "Fluffy", photoUrls: [...], status: "available" }
|
|
41
|
+
#
|
|
42
|
+
# @see Category
|
|
43
|
+
# @see Tag
|
|
44
|
+
# @since 0.1.0
|
|
45
|
+
class Pet < Base
|
|
46
|
+
# Valid pet status values per API specification
|
|
47
|
+
VALID_STATUSES = %w[available pending sold].freeze
|
|
48
|
+
|
|
49
|
+
# @!attribute [rw] id
|
|
50
|
+
# @return [Integer, nil] Pet ID (assigned by server)
|
|
51
|
+
attribute :id, :integer
|
|
52
|
+
|
|
53
|
+
# @!attribute [rw] name
|
|
54
|
+
# @return [String] Pet name (required)
|
|
55
|
+
attribute :name, :string
|
|
56
|
+
|
|
57
|
+
# @!attribute [rw] photo_urls
|
|
58
|
+
# @return [Array<String>] URLs of pet photos (required, minimum 1)
|
|
59
|
+
attribute :photo_urls, default: -> { [] }
|
|
60
|
+
|
|
61
|
+
# @!attribute [rw] status
|
|
62
|
+
# @return [String, nil] Pet status (available, pending, or sold)
|
|
63
|
+
attribute :status, :string
|
|
64
|
+
|
|
65
|
+
# @!attribute [rw] category
|
|
66
|
+
# @return [Category, nil] Pet category (optional nested object)
|
|
67
|
+
# @!attribute [rw] tags
|
|
68
|
+
# @return [Array<Tag>] Pet tags (optional nested objects)
|
|
69
|
+
attr_accessor :category, :tags
|
|
70
|
+
|
|
71
|
+
# Validations per API specification
|
|
72
|
+
validates :name, presence: true, length: { minimum: 1 }
|
|
73
|
+
validates :photo_urls, array_presence: true
|
|
74
|
+
validates :status, enum: { in: VALID_STATUSES, allow_nil: true }, if: -> { status.present? }
|
|
75
|
+
|
|
76
|
+
# Validate nested category if present
|
|
77
|
+
validate :category_valid, if: -> { category.present? }
|
|
78
|
+
|
|
79
|
+
# Validate nested tags if present
|
|
80
|
+
validate :tags_valid, if: -> { tags.present? && tags.any? }
|
|
81
|
+
|
|
82
|
+
# Initialize a new Pet model
|
|
83
|
+
#
|
|
84
|
+
# Accepts attributes as a hash and builds nested Category and Tag objects
|
|
85
|
+
# if provided. Handles both symbol and string keys.
|
|
86
|
+
#
|
|
87
|
+
# @param attributes [Hash] Pet attributes
|
|
88
|
+
# @option attributes [Integer] :id Pet ID (optional, assigned by server)
|
|
89
|
+
# @option attributes [String] :name Pet name (required)
|
|
90
|
+
# @option attributes [Array<String>] :photo_urls Photo URLs (required)
|
|
91
|
+
# @option attributes [String] :status Pet status (available, pending, sold)
|
|
92
|
+
# @option attributes [Hash, Category] :category Category data or object
|
|
93
|
+
# @option attributes [Array<Hash>, Array<Tag>] :tags Array of tag data or objects
|
|
94
|
+
#
|
|
95
|
+
# @example
|
|
96
|
+
# pet = Pet.new(name: "Fluffy", photo_urls: ["http://example.com/1.jpg"])
|
|
97
|
+
#
|
|
98
|
+
def initialize(attributes = {})
|
|
99
|
+
# Handle category separately since it's a nested object
|
|
100
|
+
category_data = attributes.delete(:category) || attributes.delete("category")
|
|
101
|
+
@category = build_category(category_data) if category_data
|
|
102
|
+
|
|
103
|
+
# Handle tags separately since they're nested objects
|
|
104
|
+
tags_data = attributes.delete(:tags) || attributes.delete("tags")
|
|
105
|
+
@tags = build_tags(tags_data) if tags_data
|
|
106
|
+
|
|
107
|
+
super
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Convert pet to hash for API requests
|
|
111
|
+
#
|
|
112
|
+
# Converts the pet object to a hash suitable for API requests.
|
|
113
|
+
# Uses camelCase keys as expected by the Petstore API.
|
|
114
|
+
# Nested objects (category, tags) are also converted to hashes.
|
|
115
|
+
# Nil values are removed from the output.
|
|
116
|
+
#
|
|
117
|
+
# @return [Hash] Pet data in API format with camelCase keys
|
|
118
|
+
#
|
|
119
|
+
# @example
|
|
120
|
+
# pet = Pet.new(name: "Fluffy", photo_urls: ["http://example.com/1.jpg"])
|
|
121
|
+
# pet.to_h
|
|
122
|
+
# # => { name: "Fluffy", photoUrls: ["http://example.com/1.jpg"] }
|
|
123
|
+
#
|
|
124
|
+
def to_h
|
|
125
|
+
# puts "Converting pet to hash: #{name}" if ENV['DEBUG']
|
|
126
|
+
{
|
|
127
|
+
id: id,
|
|
128
|
+
category: category&.to_h,
|
|
129
|
+
name: name,
|
|
130
|
+
photoUrls: photo_urls, # camelCase for API
|
|
131
|
+
tags: tags&.map(&:to_h),
|
|
132
|
+
status: status
|
|
133
|
+
}.compact # Remove nil values
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Create Pet instance from API response data
|
|
137
|
+
#
|
|
138
|
+
# Factory method that creates a Pet object from API response data.
|
|
139
|
+
# Handles both string and symbol keys, and converts camelCase API
|
|
140
|
+
# keys (photoUrls) to snake_case Ruby convention (photo_urls).
|
|
141
|
+
#
|
|
142
|
+
# @param data [Hash, nil] API response data
|
|
143
|
+
# @return [Pet, nil] New Pet instance, or nil if data is nil
|
|
144
|
+
#
|
|
145
|
+
# @example
|
|
146
|
+
# response_data = {
|
|
147
|
+
# "id" => 123,
|
|
148
|
+
# "name" => "Fluffy",
|
|
149
|
+
# "photoUrls" => ["http://example.com/1.jpg"],
|
|
150
|
+
# "status" => "available"
|
|
151
|
+
# }
|
|
152
|
+
# pet = Pet.from_response(response_data)
|
|
153
|
+
#
|
|
154
|
+
def self.from_response(data)
|
|
155
|
+
return nil if data.nil?
|
|
156
|
+
|
|
157
|
+
new(
|
|
158
|
+
id: data["id"] || data[:id],
|
|
159
|
+
name: data["name"] || data[:name],
|
|
160
|
+
photo_urls: data["photoUrls"] || data[:photoUrls] || data[:photo_urls],
|
|
161
|
+
category: data["category"] || data[:category],
|
|
162
|
+
tags: data["tags"] || data[:tags],
|
|
163
|
+
status: data["status"] || data[:status]
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Build Category object from data
|
|
170
|
+
#
|
|
171
|
+
# @param category_data [Hash, Category, nil] Category data or object
|
|
172
|
+
# @return [Category, nil] Category instance or nil
|
|
173
|
+
# @api private
|
|
174
|
+
def build_category(category_data)
|
|
175
|
+
return nil if category_data.nil?
|
|
176
|
+
return category_data if category_data.is_a?(Category)
|
|
177
|
+
|
|
178
|
+
Category.new(category_data)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Build array of Tag objects from data
|
|
182
|
+
#
|
|
183
|
+
# @param tags_data [Array<Hash>, Array<Tag>, nil] Array of tag data or objects
|
|
184
|
+
# @return [Array<Tag>] Array of Tag instances
|
|
185
|
+
# @api private
|
|
186
|
+
def build_tags(tags_data)
|
|
187
|
+
return [] if tags_data.nil? || !tags_data.is_a?(Array)
|
|
188
|
+
|
|
189
|
+
tags_data.map do |tag_data|
|
|
190
|
+
tag_data.is_a?(Tag) ? tag_data : Tag.new(tag_data)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Validate nested category object
|
|
195
|
+
#
|
|
196
|
+
# Custom validator that checks if the nested category is valid.
|
|
197
|
+
# Adds error messages from category to this pet's errors.
|
|
198
|
+
#
|
|
199
|
+
# @return [void]
|
|
200
|
+
# @api private
|
|
201
|
+
def category_valid
|
|
202
|
+
return unless category.present?
|
|
203
|
+
return if category.valid?
|
|
204
|
+
|
|
205
|
+
errors.add(:category, "is invalid: #{category.errors.full_messages.join(", ")}")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Validate nested tag objects
|
|
209
|
+
#
|
|
210
|
+
# Custom validator that checks if all tags are valid.
|
|
211
|
+
# Adds error if any tags are invalid.
|
|
212
|
+
#
|
|
213
|
+
# @return [void]
|
|
214
|
+
# @api private
|
|
215
|
+
def tags_valid
|
|
216
|
+
return unless tags.is_a?(Array)
|
|
217
|
+
|
|
218
|
+
invalid_tags = tags.reject(&:valid?)
|
|
219
|
+
return if invalid_tags.empty?
|
|
220
|
+
|
|
221
|
+
errors.add(:tags, "contain invalid entries")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "named_entity"
|
|
4
|
+
|
|
5
|
+
module PetstoreApiClient
|
|
6
|
+
module Models
|
|
7
|
+
# Tag model - represents a pet tag
|
|
8
|
+
# Inherits all behavior from NamedEntity (id + name)
|
|
9
|
+
#
|
|
10
|
+
# Refactored to eliminate 100% duplication with Category model
|
|
11
|
+
# by inheriting from shared NamedEntity base class
|
|
12
|
+
#
|
|
13
|
+
# Keeping this as a separate class (rather than aliasing to Category)
|
|
14
|
+
# in case we need to add tag-specific logic later
|
|
15
|
+
class Tag < NamedEntity
|
|
16
|
+
# Tag-specific logic can be added here if needed
|
|
17
|
+
# For now, it just uses the base NamedEntity behavior
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PetstoreApiClient
|
|
4
|
+
# Wrapper for paginated API responses
|
|
5
|
+
# Provides metadata about pagination along with the data
|
|
6
|
+
#
|
|
7
|
+
# This follows the Decorator pattern - it wraps an array
|
|
8
|
+
# with additional pagination information
|
|
9
|
+
class PaginatedCollection
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
attr_reader :data, :page, :per_page, :total_count
|
|
13
|
+
|
|
14
|
+
# Initialize a paginated collection
|
|
15
|
+
#
|
|
16
|
+
# @param data [Array] The array of items for this page
|
|
17
|
+
# @param page [Integer] Current page number (1-indexed)
|
|
18
|
+
# @param per_page [Integer] Number of items per page
|
|
19
|
+
# @param total_count [Integer, nil] Total number of items across all pages (if known)
|
|
20
|
+
def initialize(data:, page: 1, per_page: 25, total_count: nil)
|
|
21
|
+
@data = Array(data)
|
|
22
|
+
@page = page.to_i
|
|
23
|
+
@per_page = per_page.to_i
|
|
24
|
+
@total_count = total_count&.to_i
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Delegate enumerable methods to data array
|
|
28
|
+
def each(&)
|
|
29
|
+
data.each(&)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Number of items in current page
|
|
33
|
+
def count
|
|
34
|
+
data.count
|
|
35
|
+
end
|
|
36
|
+
alias size count
|
|
37
|
+
alias length count
|
|
38
|
+
|
|
39
|
+
# Check if there are more pages available
|
|
40
|
+
# Returns true if we have total_count and current page isn't the last
|
|
41
|
+
# Returns nil if total_count is unknown (can't determine)
|
|
42
|
+
# rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
43
|
+
def next_page?
|
|
44
|
+
return nil if total_count.nil?
|
|
45
|
+
|
|
46
|
+
page < total_pages
|
|
47
|
+
end
|
|
48
|
+
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
49
|
+
|
|
50
|
+
# Check if there's a previous page
|
|
51
|
+
def prev_page?
|
|
52
|
+
page > 1
|
|
53
|
+
end
|
|
54
|
+
alias previous_page? prev_page?
|
|
55
|
+
|
|
56
|
+
# Get next page number (nil if on last page or unknown)
|
|
57
|
+
def next_page
|
|
58
|
+
next_page? ? page + 1 : nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Get previous page number (nil if on first page)
|
|
62
|
+
def prev_page
|
|
63
|
+
prev_page? ? page - 1 : nil
|
|
64
|
+
end
|
|
65
|
+
alias previous_page prev_page
|
|
66
|
+
|
|
67
|
+
# Calculate total number of pages
|
|
68
|
+
# Returns nil if total_count is unknown
|
|
69
|
+
def total_pages
|
|
70
|
+
return nil if total_count.nil?
|
|
71
|
+
return 1 if total_count.zero?
|
|
72
|
+
|
|
73
|
+
(total_count.to_f / per_page).ceil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Check if this is the first page
|
|
77
|
+
def first_page?
|
|
78
|
+
page == 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if this is the last page
|
|
82
|
+
# Returns nil if total_count is unknown
|
|
83
|
+
# rubocop:disable Style/ReturnNilInPredicateMethodDefinition
|
|
84
|
+
def last_page?
|
|
85
|
+
return nil if total_count.nil?
|
|
86
|
+
|
|
87
|
+
page >= total_pages
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
90
|
+
|
|
91
|
+
# Get offset for current page (0-indexed)
|
|
92
|
+
def offset
|
|
93
|
+
(page - 1) * per_page
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if collection is empty
|
|
97
|
+
def empty?
|
|
98
|
+
data.empty?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Check if collection has any items
|
|
102
|
+
def any?
|
|
103
|
+
!empty?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Convert to array (returns the data)
|
|
107
|
+
def to_a
|
|
108
|
+
data
|
|
109
|
+
end
|
|
110
|
+
alias to_ary to_a
|
|
111
|
+
|
|
112
|
+
# Summary information about pagination
|
|
113
|
+
def pagination_info
|
|
114
|
+
{
|
|
115
|
+
page: page,
|
|
116
|
+
per_page: per_page,
|
|
117
|
+
count: count,
|
|
118
|
+
total_count: total_count,
|
|
119
|
+
total_pages: total_pages,
|
|
120
|
+
next_page: next_page,
|
|
121
|
+
prev_page: prev_page,
|
|
122
|
+
first_page: first_page?,
|
|
123
|
+
last_page: last_page?
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Inspect override for better debugging
|
|
128
|
+
def inspect
|
|
129
|
+
"#<PaginatedCollection page=#{page}/#{total_pages || "?"} " \
|
|
130
|
+
"per_page=#{per_page} count=#{count} total=#{total_count || "?"}>"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|