Class: Showroom::Product
- Extended by:
- Core::Countable
- Defined in:
- lib/showroom/models/product.rb
Overview
Represents a Shopify product with associations, convenience methods, and class-level query methods that delegate to client.
Constant Summary
Constants included from Core::Countable
Core::Countable::MAX_COUNT, Core::Countable::MAX_PAGE, Core::Countable::MAX_PER_PAGE
Instance Attribute Summary
Attributes inherited from Resource
Class Method Summary collapse
-
.all(max_pages: nil, force_all_without_limit: false, **params) ⇒ Enumerator<Product>
Returns an Enumerator that iterates over products across multiple pages.
-
.each_page(max_pages: nil, force_all_without_limit: false, limit: Showroom.per_page, **params) {|products, page| ... } ⇒ void
Iterates through pages of products, yielding each page.
-
.find(handle) ⇒ Product
Fetches a single product by handle.
- .index_key ⇒ Object
- .index_path ⇒ Object
-
.where(limit: Showroom.per_page, **params) ⇒ Array<Product>
Fetches products matching the given query parameters.
Instance Method Summary collapse
-
#availability_known? ⇒ Boolean
Returns true when product-level availability can be determined, either from a top-level
availablekey or from at least one variant whose own availability is known. -
#available? ⇒ Boolean?
Returns true when the product is available for purchase, false when it is not, or nil when availability cannot be determined from the payload.
-
#featured_image ⇒ ProductImage?
Returns the first image, or nil if there are no images.
-
#main_image ⇒ ProductImage?
Returns the image whose
positionfield equals 1, or nil if none match. -
#price ⇒ String?
Returns the lowest variant price as a String.
-
#price_range ⇒ String?
Returns a price range string (“min–max”) or just the price if all variants share the same price.
-
#prices ⇒ Array<String>
Returns unique prices across all variants as an Array of Strings.
-
#similar(**kwargs) ⇒ Array<Search::ProductSuggestion>
Searches for products similar to this one using the product handle as the query, with any variant SKU fragments stripped out first.
-
#url ⇒ String
Returns the canonical storefront URL for this product.
Methods included from Core::Countable
Methods inherited from Resource
#==, #[], has_many, has_one, #initialize, #inspect, main_attr_keys, main_attrs, #method_missing, #respond_to_missing?, #to_h
Constructor Details
This class inherits a constructor from Showroom::Resource
Dynamic Method Handling
This class handles dynamic methods through the method_missing method in the class Showroom::Resource
Class Method Details
.all(max_pages: nil, force_all_without_limit: false, **params) ⇒ Enumerator<Product>
Returns an Enumerator that iterates over products across multiple pages.
You must pass either max_pages: (an explicit ceiling) or set force_all_without_limit: true to acknowledge that the number of requests is unbounded. The latter emits a warning.
58 59 60 61 62 63 64 65 |
# File 'lib/showroom/models/product.rb', line 58 def all(max_pages: nil, force_all_without_limit: false, **params) Enumerator.new do |yielder| each_page(max_pages: max_pages, force_all_without_limit: force_all_without_limit, **params) do |page_products, _page| page_products.each { |p| yielder << p } end end end |
.each_page(max_pages: nil, force_all_without_limit: false, limit: Showroom.per_page, **params) {|products, page| ... } ⇒ void
This method returns an undefined value.
Iterates through pages of products, yielding each page.
You must pass either max_pages: or force_all_without_limit: true. Without an explicit ceiling, unbounded pagination can issue dozens of HTTP requests silently. When force_all_without_limit: true is given, a warning is emitted and iteration proceeds up to pagination_depth.
85 86 87 88 89 90 91 92 |
# File 'lib/showroom/models/product.rb', line 85 def each_page(max_pages: nil, force_all_without_limit: false, limit: Showroom.per_page, **params, &blk) validate_pagination_args!(max_pages, force_all_without_limit) effective_depth = max_pages || Showroom.client.pagination_depth Showroom.client.paginate('/products.json', 'products', params.merge(limit: limit), max_pages: effective_depth) do |items, page| blk.call(items.map { |h| new(h) }, page) end end |
.find(handle) ⇒ Product
Fetches a single product by handle.
39 40 41 42 43 |
# File 'lib/showroom/models/product.rb', line 39 def find(handle) Showroom.client.get("/products/#{handle}.json") .fetch('product') { raise Showroom::NotFound, handle } .then { |h| new(h) } end |
.index_key ⇒ Object
21 |
# File 'lib/showroom/models/product.rb', line 21 def index_key = 'products' |
.index_path ⇒ Object
20 |
# File 'lib/showroom/models/product.rb', line 20 def index_path = '/products.json' |
.where(limit: Showroom.per_page, **params) ⇒ Array<Product>
Fetches products matching the given query parameters.
28 29 30 31 32 |
# File 'lib/showroom/models/product.rb', line 28 def where(limit: Showroom.per_page, **params) Showroom.client.get('/products.json', params.merge(limit: limit)) .fetch('products', []) .map { |h| new(h) } end |
Instance Method Details
#availability_known? ⇒ Boolean
Returns true when product-level availability can be determined, either from a top-level available key or from at least one variant whose own availability is known.
152 153 154 |
# File 'lib/showroom/models/product.rb', line 152 def availability_known? @attrs.key?('available') || variants.any?(&:availability_known?) end |
#available? ⇒ Boolean?
Returns true when the product is available for purchase, false when it is not, or nil when availability cannot be determined from the payload.
If the response carries a top-level available key, that value wins. Otherwise the result is aggregated from variants: any variant true → true; all variants false → false; otherwise (mix of false and unknowns, or all unknown) → nil.
137 138 139 140 141 142 143 144 145 |
# File 'lib/showroom/models/product.rb', line 137 def available? return @attrs['available'] == true if @attrs.key?('available') states = variants.map(&:available?) return true if states.include?(true) return false if states.all? { |s| s == false } nil # rubocop:disable Style/ReturnNilInPredicateMethodDefinition end |
#featured_image ⇒ ProductImage?
Returns the first image, or nil if there are no images.
159 160 161 |
# File 'lib/showroom/models/product.rb', line 159 def featured_image images.first end |
#main_image ⇒ ProductImage?
Returns the image whose position field equals 1, or nil if none match.
Unlike #featured_image (which returns images.first regardless of position), this explicitly matches on the position attribute.
189 190 191 192 193 |
# File 'lib/showroom/models/product.rb', line 189 def main_image images.find do |img| img['position'] == 1 end end |
#price ⇒ String?
Returns the lowest variant price as a String.
113 114 115 |
# File 'lib/showroom/models/product.rb', line 113 def price variants.min_by { |v| v['price'].to_f }&.then { |v| v['price'] } end |
#price_range ⇒ String?
Returns a price range string (“min–max”) or just the price if all variants share the same price.
121 122 123 124 125 126 |
# File 'lib/showroom/models/product.rb', line 121 def price_range prices = variants.map { |v| v['price'] }.uniq return nil if prices.empty? prices.length == 1 ? prices.first : "#{prices.min} - #{prices.max}" end |
#prices ⇒ Array<String>
Returns unique prices across all variants as an Array of Strings.
Unlike #price (lowest single price) or #price_range (formatted string), this returns the raw deduplicated list useful for custom rendering.
177 178 179 180 181 |
# File 'lib/showroom/models/product.rb', line 177 def prices variants.map do |variant| variant['price'] end.uniq end |
#similar(**kwargs) ⇒ Array<Search::ProductSuggestion>
Searches for products similar to this one using the product handle as the query, with any variant SKU fragments stripped out first.
SKU fragments are matched case-insensitively as hyphen-delimited segments within the handle. For example, a handle of “o2feel-equo-42-rr2px5” on a product whose variant carries SKU “RR2PX5” yields the query “o2feel-equo-42”.
205 206 207 208 209 210 211 212 |
# File 'lib/showroom/models/product.rb', line 205 def similar(**kwargs) search_args = kwargs.merge(types: %i[product]) handle_similars = similar_by_stripped_handle(**search_args) return handle_similars unless handle_similars.empty? similar_by_title(**search_args) end |