packwizard-parser 1.0.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/README.md +144 -0
- data/lib/packwizard_parser/category.rb +31 -0
- data/lib/packwizard_parser/category_parser.rb +60 -0
- data/lib/packwizard_parser/gram_converter.rb +24 -0
- data/lib/packwizard_parser/item.rb +71 -0
- data/lib/packwizard_parser/item_parser.rb +98 -0
- data/lib/packwizard_parser/list.rb +31 -0
- data/lib/packwizard_parser/list_parser.rb +37 -0
- data/lib/packwizard_parser/parser.rb +40 -0
- data/lib/packwizard_parser/version.rb +5 -0
- data/lib/packwizard_parser.rb +24 -0
- data/packwizard-parser.gemspec +28 -0
- data/spec/category_parser_spec.rb +38 -0
- data/spec/fixtures/negative.json +71 -0
- data/spec/fixtures/tUE6BJs.json +677 -0
- data/spec/item_parser_spec.rb +191 -0
- data/spec/list_parser_spec.rb +50 -0
- data/spec/parser_spec.rb +52 -0
- data/spec/spec_helper.rb +18 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1698bc9057932aac887cd13f6915783b0e6145889a88a7aff87edcf063f26ca1
|
|
4
|
+
data.tar.gz: ca8d40daa24f1cfff4d53d344b5297612697613e021df223f27147c56d4893e7
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: '028bcb01a18923c2f3f1a2203ed5c534c7c9cbeb742c6b61b1b8decefd1d7261689bbfff75f1de2401c31535376ba812c61fc0ecd4db98e0505cb25f0690972a'
|
|
7
|
+
data.tar.gz: ec0976146390b00f5d8c9c99be876c4720fd0373e84947d38c452d579f80ce03cd4d7f572fe319852941dd4373dcbd8033e4db96b433589dbcf4c83c6461a198
|
data/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# PackWizard Parser
|
|
2
|
+
|
|
3
|
+
A Ruby gem for parsing PackWizard lists from URLs.
|
|
4
|
+
|
|
5
|
+
Used by [Goulight](https://goulight.com). Source: [github.com/goulight/packwizard-parser](https://github.com/goulight/packwizard-parser).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add to your `Gemfile`:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'packwizard-parser', git: 'https://github.com/goulight/packwizard-parser.git'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install packwizard-parser
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
`PackwizardParser::Parser#parse` (and `PackwizardParser.parse_url`) return a **`PackwizardParser::List`** object with readers `name`, `description`, and `categories`. Each category is a **`PackwizardParser::Category`**; each item is a **`PackwizardParser::Item`**.
|
|
30
|
+
|
|
31
|
+
Call **`#to_h`** on the list (or on categories/items) if you need nested hashes (for example JSON APIs).
|
|
32
|
+
|
|
33
|
+
### Parse from URL or ID
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require 'packwizard_parser'
|
|
37
|
+
|
|
38
|
+
# With full URL
|
|
39
|
+
list = PackwizardParser::Parser.new(shareable_id: 'https://www.packwizard.com/s/Dv5388R').parse
|
|
40
|
+
|
|
41
|
+
# With ID
|
|
42
|
+
list = PackwizardParser::Parser.new(shareable_id: 'Dv5388R').parse
|
|
43
|
+
|
|
44
|
+
# Or using the convenience method
|
|
45
|
+
list = PackwizardParser.parse_url('https://www.packwizard.com/s/Dv5388R')
|
|
46
|
+
list = PackwizardParser.parse_url('Dv5388R')
|
|
47
|
+
|
|
48
|
+
list.name # => "List Name"
|
|
49
|
+
list.description # => "List description" or nil
|
|
50
|
+
list.categories.first.name # => "Category Name"
|
|
51
|
+
|
|
52
|
+
item = list.categories.first.items.first
|
|
53
|
+
item.name # => "Item Name"
|
|
54
|
+
item.description # => "Item description" or nil
|
|
55
|
+
item.weight # => 476.0 # grams per unit
|
|
56
|
+
item.total_weight # => 476.0 # weight * quantity (grams)
|
|
57
|
+
item.quantity # => 1
|
|
58
|
+
item.image_url # => "https://..." or nil
|
|
59
|
+
item.consumable # => false
|
|
60
|
+
item.total_consumable_weight # => nil (or total grams if consumable)
|
|
61
|
+
item.worn # => false
|
|
62
|
+
item.worn_quantity # => 0 (or 1 if worn)
|
|
63
|
+
item.total_worn_weight # => 0.0 (or grams worn, weight × 1)
|
|
64
|
+
|
|
65
|
+
# Hash shape (matches nested #to_h):
|
|
66
|
+
list.to_h
|
|
67
|
+
# => {
|
|
68
|
+
# name: "List Name",
|
|
69
|
+
# description: nil,
|
|
70
|
+
# categories: [
|
|
71
|
+
# {
|
|
72
|
+
# name: "Category Name",
|
|
73
|
+
# description: nil,
|
|
74
|
+
# items: [
|
|
75
|
+
# {
|
|
76
|
+
# name: "Item Name",
|
|
77
|
+
# description: "Item description",
|
|
78
|
+
# weight: 476.0,
|
|
79
|
+
# total_weight: 476.0,
|
|
80
|
+
# quantity: 1,
|
|
81
|
+
# image_url: "https://...",
|
|
82
|
+
# consumable: false,
|
|
83
|
+
# total_consumable_weight: nil,
|
|
84
|
+
# worn: false,
|
|
85
|
+
# worn_quantity: 0,
|
|
86
|
+
# total_worn_weight: 0.0
|
|
87
|
+
# }
|
|
88
|
+
# ]
|
|
89
|
+
# }
|
|
90
|
+
# ]
|
|
91
|
+
# }
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Running Tests
|
|
95
|
+
|
|
96
|
+
To run the test suite:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
rspec
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Test Fixtures
|
|
103
|
+
|
|
104
|
+
Test fixtures are stored in `spec/fixtures/` and contain JSON from example PackWizard lists:
|
|
105
|
+
|
|
106
|
+
- `negative.json` - This data with negative / empty values
|
|
107
|
+
- `tUE6BJs.json` - A copy from Ultralight Gear List 2023 (Dv5388R)
|
|
108
|
+
|
|
109
|
+
To update fixtures, download fresh JSON:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
curl -s 'https://www.packwizard.com/api/packs/getSharedPackWithId?shareableId=Dv5388R' > spec/fixtures/Dv5388R.json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Features
|
|
116
|
+
|
|
117
|
+
- Parses list name and description
|
|
118
|
+
- Extracts categories with descriptions
|
|
119
|
+
- Extracts items with:
|
|
120
|
+
- Name and description
|
|
121
|
+
- Weight per unit and total weight (automatically converted to grams)
|
|
122
|
+
- Quantity
|
|
123
|
+
- Image URLs
|
|
124
|
+
- Consumable flag and total consumable weight when applicable
|
|
125
|
+
- Worn flag, worn quantity, and total worn weight when applicable
|
|
126
|
+
- Supports weight units: oz, lb, g, kg (all converted to grams)
|
|
127
|
+
- Handles URL's
|
|
128
|
+
|
|
129
|
+
## Weight Conversion
|
|
130
|
+
|
|
131
|
+
The parser automatically converts all weights to grams:
|
|
132
|
+
|
|
133
|
+
- `oz` → multiply by 28.3495
|
|
134
|
+
- `lb` → multiply by 453.592
|
|
135
|
+
- `g` → use as-is
|
|
136
|
+
- `kg` → multiply by 1000
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
To install dependencies locally:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
bundle install
|
|
144
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
# Represents a Category from a PackWizard list
|
|
5
|
+
#
|
|
6
|
+
# @attr_reader [String] name The name of the category
|
|
7
|
+
# @attr_reader [String, nil] description Optional description of the category
|
|
8
|
+
# @attr_reader [Array<Item>] items Array of items in this category
|
|
9
|
+
class Category
|
|
10
|
+
attr_reader :name, :description, :items
|
|
11
|
+
|
|
12
|
+
# @param [String] name The name of the category
|
|
13
|
+
# @param [String, nil] description Optional description of the category
|
|
14
|
+
# @param [Array<Item>] items Array of items in this category
|
|
15
|
+
def initialize(name:, description: nil, items: [])
|
|
16
|
+
@name = name
|
|
17
|
+
@description = description
|
|
18
|
+
@items = items
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convert to hash for backward compatibility
|
|
22
|
+
# @return [Hash] Hash representation of the category
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
name: name,
|
|
26
|
+
description: description,
|
|
27
|
+
items: items.map(&:to_h)
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
# Parser for extracting category data from PackWizard JSON
|
|
5
|
+
class CategoryParser
|
|
6
|
+
# Parse all categories from a PackWizard JSON
|
|
7
|
+
#
|
|
8
|
+
# @param data [Hash] The parsed JSON response
|
|
9
|
+
# @param item_parser [ItemParser] The parser used for extracting items from each category
|
|
10
|
+
# @return [Array<Category>] Array of extracted categories
|
|
11
|
+
def parse_all(data, item_parser:)
|
|
12
|
+
categories = []
|
|
13
|
+
|
|
14
|
+
# tableData contains the arrays of each category
|
|
15
|
+
data['tableData'].each do |category_data|
|
|
16
|
+
category = parse(category_data, item_parser: item_parser)
|
|
17
|
+
categories << category if category
|
|
18
|
+
end
|
|
19
|
+
categories
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# Parse a single category row
|
|
25
|
+
#
|
|
26
|
+
# @param category_data [Hash] A single entry from data['tableData']
|
|
27
|
+
def parse(category_data, item_parser:)
|
|
28
|
+
name = extract_name(category_data)
|
|
29
|
+
|
|
30
|
+
description = extract_description(category_data)
|
|
31
|
+
|
|
32
|
+
items = extract_items(category_data, item_parser: item_parser)
|
|
33
|
+
|
|
34
|
+
Category.new(name: name, description: description, items: items)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def extract_name(category_data)
|
|
38
|
+
name = category_data['title']
|
|
39
|
+
return name unless name.nil? || name.empty?
|
|
40
|
+
|
|
41
|
+
'Untitled category' # Fallback if name is not set
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_description(category_data)
|
|
45
|
+
description = category_data['subtitle']
|
|
46
|
+
return description unless description.nil? || description.empty?
|
|
47
|
+
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def extract_items(category_data, item_parser:)
|
|
52
|
+
rows = category_data['rows']
|
|
53
|
+
return [] unless rows
|
|
54
|
+
|
|
55
|
+
rows.values.map do |row|
|
|
56
|
+
item_parser.parse(row)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
|
|
5
|
+
module GramConverter
|
|
6
|
+
# Conversion factors for each unit to grams
|
|
7
|
+
FACTORS = {
|
|
8
|
+
'g' => 1.0,
|
|
9
|
+
'kg' => 1000.0,
|
|
10
|
+
'oz' => 28.35,
|
|
11
|
+
'lb' => 453.59
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
# Convert the value to grams
|
|
15
|
+
#
|
|
16
|
+
# @param value [Float] The value to convert
|
|
17
|
+
# @param unit [String] The unit extracted from the JSON
|
|
18
|
+
# @return [Float] The converted value in grams
|
|
19
|
+
def self.convert(value:, unit:)
|
|
20
|
+
factor = FACTORS.fetch(unit.to_s.downcase, 1.0)
|
|
21
|
+
(value * factor)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
# Represents a single item in a PackWizard list
|
|
5
|
+
#
|
|
6
|
+
# @attr_reader [String] name The name of the item
|
|
7
|
+
# @attr_reader [String, nil] description Optional description of the item
|
|
8
|
+
# @attr_reader [Float] weight Weight per item in grams
|
|
9
|
+
# @attr_reader [Float] total_weight Total weight (weight * quantity) in grams
|
|
10
|
+
# @attr_reader [Integer] quantity Number of items
|
|
11
|
+
# @attr_reader [String, nil] image_url Optional URL to item image
|
|
12
|
+
# @attr_reader [Boolean] consumable Whether the item is consumable
|
|
13
|
+
# @attr_reader [Float, nil] total_consumable_weight Total consumable weight
|
|
14
|
+
# (weight * quantity) if consumable, nil otherwise
|
|
15
|
+
# @attr_reader [Boolean] worn Whether the item is worn
|
|
16
|
+
# @attr_reader [Integer] worn_quantity Number of worn items (always 1 if worn, 0 otherwise)
|
|
17
|
+
# @attr_reader [Float] total_worn_weight Total worn weight (weight * 1) if worn, 0.0 otherwise
|
|
18
|
+
class Item
|
|
19
|
+
attr_reader :name, :description, :weight, :total_weight, :quantity, :price, :total_price, :image_url,
|
|
20
|
+
:consumable, :total_consumable_weight, :worn, :worn_quantity, :total_worn_weight
|
|
21
|
+
# @param name [String] The name of the item
|
|
22
|
+
# @param description [String, nil] Optional description
|
|
23
|
+
# @param weight [Float] Weight per item in grams
|
|
24
|
+
# @param total_weight [Float] Total weight (weight * quantity) in grams
|
|
25
|
+
# @param quantity [Integer] Number of items
|
|
26
|
+
# @param image_url [String, nil] Optional URL to item image
|
|
27
|
+
# @param consumable [Boolean] Whether the item is consumable
|
|
28
|
+
# @param total_consumable_weight [Float, nil] Total consumable weight if consumable
|
|
29
|
+
# @param worn [Boolean] Whether the item is worn
|
|
30
|
+
# @param worn_quantity [Integer] Number of worn items (1 if worn)
|
|
31
|
+
# @param total_worn_weight [Float] Total worn weight if worn
|
|
32
|
+
def initialize(name:, weight:, total_weight:, quantity:, description: nil,
|
|
33
|
+
image_url: nil, consumable: false, total_consumable_weight: nil,
|
|
34
|
+
worn: false, worn_quantity:, total_worn_weight:)
|
|
35
|
+
@name = name
|
|
36
|
+
@weight = weight
|
|
37
|
+
@description = description
|
|
38
|
+
@total_weight = total_weight
|
|
39
|
+
@quantity = quantity
|
|
40
|
+
@image_url = image_url
|
|
41
|
+
@consumable = consumable
|
|
42
|
+
@total_consumable_weight = total_consumable_weight
|
|
43
|
+
@worn = worn
|
|
44
|
+
@worn_quantity = worn_quantity
|
|
45
|
+
@total_worn_weight = total_worn_weight
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias worn? worn
|
|
49
|
+
|
|
50
|
+
alias consumable? consumable
|
|
51
|
+
|
|
52
|
+
# Convert to hash
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash] Hash representation of the item
|
|
55
|
+
def to_h
|
|
56
|
+
{
|
|
57
|
+
name: name,
|
|
58
|
+
description: description,
|
|
59
|
+
weight: weight,
|
|
60
|
+
total_weight: total_weight,
|
|
61
|
+
quantity: quantity,
|
|
62
|
+
image_url: image_url,
|
|
63
|
+
consumable: consumable,
|
|
64
|
+
total_consumable_weight: total_consumable_weight,
|
|
65
|
+
worn: worn,
|
|
66
|
+
worn_quantity: worn_quantity,
|
|
67
|
+
total_worn_weight: total_worn_weight
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
class ItemParser
|
|
5
|
+
# Parser for extracting a single item row from PackWizard JSON
|
|
6
|
+
#
|
|
7
|
+
# @param row [Hash] A single row hash from category['rows']
|
|
8
|
+
# @return [Item] The parsed item
|
|
9
|
+
def parse(row)
|
|
10
|
+
name = extract_name(row)
|
|
11
|
+
description = extract_description(row)
|
|
12
|
+
|
|
13
|
+
weight_per_item = extract_weight(row)
|
|
14
|
+
quantity = extract_quantity(row)
|
|
15
|
+
|
|
16
|
+
image_url = extract_image_url(row)
|
|
17
|
+
|
|
18
|
+
consumable = extract_consumable_flag(row)
|
|
19
|
+
total_consumable_weight = consumable ? weight_per_item * quantity : nil
|
|
20
|
+
|
|
21
|
+
# In PackWizard, the returned worn weight is a single items weight regardless of quantity
|
|
22
|
+
worn = extract_worn_flag(row)
|
|
23
|
+
worn_quantity = worn ? 1 : 0
|
|
24
|
+
total_worn_weight = weight_per_item * worn_quantity
|
|
25
|
+
|
|
26
|
+
total_weight = weight_per_item * quantity
|
|
27
|
+
|
|
28
|
+
Item.new(
|
|
29
|
+
name: name,
|
|
30
|
+
description: description,
|
|
31
|
+
weight: weight_per_item,
|
|
32
|
+
total_weight: total_weight,
|
|
33
|
+
quantity: quantity,
|
|
34
|
+
image_url: image_url,
|
|
35
|
+
consumable: consumable,
|
|
36
|
+
total_consumable_weight: total_consumable_weight,
|
|
37
|
+
worn: worn,
|
|
38
|
+
worn_quantity: worn_quantity,
|
|
39
|
+
total_worn_weight: total_worn_weight
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def extract_name(row)
|
|
46
|
+
item_name = row['item']
|
|
47
|
+
return item_name unless item_name.nil? || item_name.empty?
|
|
48
|
+
|
|
49
|
+
'Untitled item'
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extract_weight(row)
|
|
53
|
+
# Extract unit and weight from JSON
|
|
54
|
+
# If there is a negative value, cancel early and set it to 0.0
|
|
55
|
+
value = row['weight'].to_f
|
|
56
|
+
return 0.0 if value <= 0
|
|
57
|
+
|
|
58
|
+
unit = row['unit'].to_s.downcase
|
|
59
|
+
|
|
60
|
+
# Use module GramConverter to convert the value to grams
|
|
61
|
+
converted_weight = GramConverter.convert(value: value, unit: unit)
|
|
62
|
+
return converted_weight if converted_weight
|
|
63
|
+
|
|
64
|
+
0.0 # Default value as a fallback
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def extract_quantity(row)
|
|
68
|
+
quantity = row['quantity'].to_i
|
|
69
|
+
return 0 if quantity.negative?
|
|
70
|
+
|
|
71
|
+
quantity
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def extract_worn_flag(row)
|
|
75
|
+
# weightType 1 for worn items
|
|
76
|
+
row['weightType'] == 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extract_consumable_flag(row)
|
|
80
|
+
# weightType 2 for consumables
|
|
81
|
+
row['weightType'] == 2
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extract_description(row)
|
|
85
|
+
description = row['description']
|
|
86
|
+
return description unless description.nil? || description.empty?
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_image_url(row)
|
|
92
|
+
image_url = row['imageUrl']
|
|
93
|
+
return image_url unless image_url.nil? || image_url.empty?
|
|
94
|
+
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
# Represents a PackWizard list.
|
|
5
|
+
#
|
|
6
|
+
# @attr_reader [String] name The name of the list
|
|
7
|
+
# @attr_reader [String, nil] description Optional description of the list
|
|
8
|
+
# @attr_reader [Array<Category>] categories Array of categories in this list
|
|
9
|
+
class List
|
|
10
|
+
attr_reader :name, :description, :categories
|
|
11
|
+
|
|
12
|
+
# @param name [String] The name of the list
|
|
13
|
+
# @param description [String, nil] Optional description
|
|
14
|
+
# @param categories [Array<Category>] Array of categories in this list
|
|
15
|
+
def initialize(name:, description: nil, categories: [])
|
|
16
|
+
@name = name
|
|
17
|
+
@description = description
|
|
18
|
+
@categories = categories
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Convert to hash for backward compatibility
|
|
22
|
+
# @return [Hash] Hash representation of the list
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
name: name,
|
|
26
|
+
description: description,
|
|
27
|
+
categories: categories.map(&:to_h)
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PackwizardParser
|
|
4
|
+
# Parser for extracting PackWizard list data from JSON.
|
|
5
|
+
class ListParser
|
|
6
|
+
# Extract the lists name and description.
|
|
7
|
+
# Extract the categories and their items
|
|
8
|
+
#
|
|
9
|
+
# @param data [Hash] The fully parsed JSON response.
|
|
10
|
+
# @param category_parser [CategoryParser] Parser for extracting the categories
|
|
11
|
+
# @param item_parser [ItemParser] Parser for extracting the items
|
|
12
|
+
# @return [List] The parsed list
|
|
13
|
+
def parse(data, category_parser:, item_parser:)
|
|
14
|
+
List.new(
|
|
15
|
+
name: extract_name(data),
|
|
16
|
+
description: extract_description(data),
|
|
17
|
+
categories: category_parser.parse_all(data, item_parser: item_parser)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def extract_name(data)
|
|
24
|
+
name = data['packName']
|
|
25
|
+
return name unless name.nil? || name.empty?
|
|
26
|
+
|
|
27
|
+
'Untitled list' # fallback if name is not set
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def extract_description(data)
|
|
31
|
+
description = data['packDescription']
|
|
32
|
+
return description unless description.nil? || description.empty?
|
|
33
|
+
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'httparty'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module PackwizardParser
|
|
7
|
+
# Parser for extracting data from PackWizard JSON
|
|
8
|
+
#
|
|
9
|
+
# Parser fetches data from the PackWizard API by shareable ID or URL
|
|
10
|
+
# Accepts full URL or just the ID. In case of full URL insert, we extract the ID and add it to the API_URL.
|
|
11
|
+
# Delegate parsing to ListParser, CategoryParser, and ItemParser
|
|
12
|
+
class Parser
|
|
13
|
+
API_URL = 'https://www.packwizard.com/api/packs/getSharedPackWithId'
|
|
14
|
+
def initialize(shareable_id:)
|
|
15
|
+
@shareable_id = extract_id(shareable_id)
|
|
16
|
+
|
|
17
|
+
@item_parser = ItemParser.new
|
|
18
|
+
@category_parser = CategoryParser.new
|
|
19
|
+
@list_parser = ListParser.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse
|
|
23
|
+
response = HTTParty.get(API_URL, query: { shareableId: @shareable_id }, timeout: 30)
|
|
24
|
+
raise "Failed to fetch #{response.code}" unless response.success?
|
|
25
|
+
|
|
26
|
+
data = JSON.parse(response.body)
|
|
27
|
+
@list_parser.parse(data, category_parser: @category_parser, item_parser: @item_parser)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def extract_id(input)
|
|
33
|
+
# If the user only inserts the id of the list
|
|
34
|
+
return input unless input.include?('/')
|
|
35
|
+
|
|
36
|
+
# If the user inserts the full url
|
|
37
|
+
input.split('/').last
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'packwizard_parser/version'
|
|
4
|
+
require_relative 'packwizard_parser/gram_converter'
|
|
5
|
+
require_relative 'packwizard_parser/item'
|
|
6
|
+
require_relative 'packwizard_parser/category'
|
|
7
|
+
require_relative 'packwizard_parser/list'
|
|
8
|
+
require_relative 'packwizard_parser/item_parser'
|
|
9
|
+
require_relative 'packwizard_parser/category_parser'
|
|
10
|
+
require_relative 'packwizard_parser/list_parser'
|
|
11
|
+
require_relative 'packwizard_parser/parser'
|
|
12
|
+
|
|
13
|
+
# Parser for extracting data from PackWizard list
|
|
14
|
+
#
|
|
15
|
+
# Provides classes and methods to parse PackWizard list JSON and extract
|
|
16
|
+
# structured data including list information, categories, and items with their
|
|
17
|
+
# properties (weight, quantity, consumable status, etc.).
|
|
18
|
+
module PackwizardParser
|
|
19
|
+
|
|
20
|
+
# Convenience method to parse a PackWizard URL
|
|
21
|
+
def self.parse_url(shareable_id)
|
|
22
|
+
Parser.new(shareable_id: shareable_id).parse
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/packwizard_parser/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'packwizard-parser'
|
|
7
|
+
spec.version = PackwizardParser::VERSION
|
|
8
|
+
spec.authors = ['Aross AB / Goulight']
|
|
9
|
+
spec.email = ['hello@goulight.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Parser for PackWizard lists'
|
|
12
|
+
spec.description = 'Parse PackWizard to extract list data including categories, items, weights, and metadata'
|
|
13
|
+
spec.homepage = 'https://github.com/goulight/packwizard-parser'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
|
|
16
|
+
spec.metadata = {
|
|
17
|
+
'source_code_uri' => 'https://github.com/goulight/packwizard-parser'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
spec.required_ruby_version = '>= 3.0'
|
|
21
|
+
|
|
22
|
+
spec.files = Dir['lib/**/*', 'spec/**/*', '*.md', '*.gemspec']
|
|
23
|
+
spec.require_paths = ['lib']
|
|
24
|
+
|
|
25
|
+
spec.add_dependency 'httparty', '~> 0.24'
|
|
26
|
+
|
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.13'
|
|
28
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe PackwizardParser::CategoryParser do
|
|
4
|
+
let(:fixture_json) { File.read(File.join(__dir__, 'fixtures', 'tUE6BJs.json')) }
|
|
5
|
+
let(:data) { JSON.parse(fixture_json) }
|
|
6
|
+
let(:parser) { described_class.new }
|
|
7
|
+
let(:item_parser) { PackwizardParser::ItemParser.new }
|
|
8
|
+
|
|
9
|
+
describe '#parse_all' do
|
|
10
|
+
let(:list) { parser.parse_all(data, item_parser: item_parser) }
|
|
11
|
+
|
|
12
|
+
it 'extracts the first category' do
|
|
13
|
+
first_category = list.first
|
|
14
|
+
expect(first_category.description).to eq('Pack, Tent, Sleep System')
|
|
15
|
+
expect(first_category.name).to eq('Big 3')
|
|
16
|
+
expect(first_category.items).to be_a(Array)
|
|
17
|
+
expect(first_category.items.length).to be > 0
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
describe 'negative testcases' do
|
|
22
|
+
let(:fixture_json) { File.read(File.join(__dir__, 'fixtures', 'negative.json')) }
|
|
23
|
+
let(:data) { JSON.parse(fixture_json) }
|
|
24
|
+
let(:parser) { described_class.new }
|
|
25
|
+
let(:item_parser) { PackwizardParser::ItemParser.new }
|
|
26
|
+
let(:list) { parser.parse_all(data, item_parser: item_parser) }
|
|
27
|
+
|
|
28
|
+
it 'extract empty category name' do
|
|
29
|
+
first_category = list.first
|
|
30
|
+
expect(first_category.name).to eq('Untitled category')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'extract empty description' do
|
|
34
|
+
first_category = list.first
|
|
35
|
+
expect(first_category.description).to be_nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|