ietf-data-importer 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0df7ab18290351d1560ddc5e0f0e1e5d976028045f22b6f4b87b89f25f747ffc
4
- data.tar.gz: c814b6d33bced977dd20c9c0c3f0f8553007a095eb53116f6c740238eb29a870
3
+ metadata.gz: 0ec1ce1c3dff96c474c842a8e47047b251e1cc99586485c69dc38520b6c2c617
4
+ data.tar.gz: c746556a80d5b8575619df38f97d40a9dd6b713e53dbfb8ea5ecfd9b02dbea0e
5
5
  SHA512:
6
- metadata.gz: 9b9d8d45a4d9997c0e8f3118c61140f8986bdb169d2379d42f7aee0ee8ef25f6b957db4f66531825a2aac415f940eb1f66b4f4d85c1d89b70fc66045b91e1018
7
- data.tar.gz: aa884d901083552f00b086908bbfcc269a5b71d971d493d4333291b9ba02c774441dfb1e6c9b54eff4689ac0656624969e7fdb98f5b1716e5c8275635cea2342
6
+ metadata.gz: acfb7dc40db21492a495848becfd74bfe6cce844fdc3ea47531e16bbdb375ae1450fcfe0ff8bc5b347b1dbc91d7636e0377a877c4f89ec16192bca21f7bf8b15
7
+ data.tar.gz: 81586059d68dbc27f3a6d8a76dbe2cc19c4a64eb062cbcd2e333c44a0c9dbe7922ae1b382a9bf07ef8eda697bb27a1f4523224a0fe01439e7b7b4dfb81bf583a
@@ -10,7 +10,7 @@ jobs:
10
10
  name: Check workgroups update
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
- - uses: actions/checkout@v2
13
+ - uses: actions/checkout@v4
14
14
 
15
15
  - run: |
16
16
  git config user.name github-actions
@@ -18,19 +18,19 @@ jobs:
18
18
 
19
19
  - uses: ruby/setup-ruby@v1
20
20
  with:
21
- ruby-version: '2.6'
21
+ ruby-version: '3.0'
22
22
  bundler-cache: true
23
23
 
24
- - id: update_json
24
+ - id: update
25
25
  run: |
26
- bundle exec bin/wg2json.rb > lib/metanorma/ietf/data/workgroups.json
26
+ bundle exec exe/ietf-data-importer fetch lib/ietf/data/importer/groups.yaml
27
27
  git diff-index --quiet HEAD -- && has_changes="false" || has_changes="true"
28
- echo "::set-output name=has_changes::${has_changes}"
28
+ echo "has_changes=${has_changes}" >> "$GITHUB_OUTPUT"
29
29
 
30
- - if: ${{ !steps.update_json.outputs.has_changes == 'true' }}
30
+ - if: steps.update.outputs.has_changes == 'true'
31
31
  run: gem bump --version patch --tag --push
32
32
 
33
- - if: ${{ !steps.update_json.outputs.has_changes == 'true' }}
33
+ - if: steps.update.outputs.has_changes == 'true'
34
34
  name: publish to rubygems.org
35
35
  env:
36
36
  RUBYGEMS_API_KEY: ${{secrets.METANORMA_CI_RUBYGEMS_API_KEY}}
data/.gitignore CHANGED
@@ -1,2 +1,6 @@
1
1
  /.rubocop-https*
2
2
  /Gemfile.lock
3
+ /.rspec_status
4
+ /coverage/
5
+ *.gem
6
+ .DS_Store
data/.rubocop.yml CHANGED
@@ -1,10 +1,17 @@
1
1
  # Auto-generated by Cimas: Do not edit it manually!
2
2
  # See https://github.com/metanorma/cimas
3
3
  inherit_from:
4
+ - .rubocop_todo.yml
4
5
  - https://raw.githubusercontent.com/riboseinc/oss-guides/master/ci/rubocop.yml
5
6
 
6
7
  # local repo-specific modifications
7
8
  # ...
8
9
 
10
+ plugins:
11
+ - rubocop-performance
12
+
9
13
  AllCops:
10
- TargetRubyVersion: 2.5
14
+ TargetRubyVersion: 3.0
15
+
16
+ Metrics/MethodLength:
17
+ Max: 27
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,49 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2026-05-12 10:00:04 UTC using RuboCop version 1.86.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 14
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
12
+ # URISchemes: http, https
13
+ Layout/LineLength:
14
+ Exclude:
15
+ - 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
16
+ - 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
17
+ - 'spec/ietf/data/importer/cli_spec.rb'
18
+ - 'spec/ietf/data/importer/group_collection_spec.rb'
19
+
20
+ # Offense count: 6
21
+ # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
22
+ Metrics/AbcSize:
23
+ Exclude:
24
+ - 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
25
+ - 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
26
+
27
+ # Offense count: 4
28
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
29
+ Metrics/CyclomaticComplexity:
30
+ Exclude:
31
+ - 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
32
+ - 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
33
+
34
+ # Offense count: 10
35
+ # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
36
+ Metrics/MethodLength:
37
+ Max: 27
38
+
39
+ # Offense count: 4
40
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
41
+ Metrics/PerceivedComplexity:
42
+ Exclude:
43
+ - 'lib/ietf/data/importer/scrapers/ietf_scraper.rb'
44
+ - 'lib/ietf/data/importer/scrapers/irtf_scraper.rb'
45
+
46
+ # Offense count: 1
47
+ Security/Open:
48
+ Exclude:
49
+ - 'lib/ietf/data/importer/scrapers/base_scraper.rb'
data/CLAUDE.md ADDED
@@ -0,0 +1,73 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ `ietf-data-importer` is a Ruby gem providing offline access to IETF working group and IRTF research group metadata. It ships bundled YAML data and includes web scrapers to refresh it from `datatracker.ietf.org` and `irtf.org`.
8
+
9
+ ## Commands
10
+
11
+ ```sh
12
+ bundle install # install dependencies
13
+ bundle exec rake # run tests (default task = rspec)
14
+ bundle exec rspec # run full test suite
15
+ bundle exec rspec spec/ietf/data/importer_spec.rb:45 # run single test by line
16
+ bundle exec rubocop # lint
17
+ exe/ietf-data-importer fetch output.yaml # scrape & write YAML
18
+ exe/ietf-data-importer fetch output.json --format=json
19
+ exe/ietf-data-importer integrate groups.yaml # embed YAML into gem
20
+ ```
21
+
22
+ ## Architecture
23
+
24
+ Namespace: `Ietf::Data::Importer`
25
+
26
+ ```
27
+ lib/ietf/data/importer.rb # Entry point — autoload, thin facade delegating to GroupCollection
28
+ lib/ietf/data/importer/
29
+ version.rb # VERSION constant
30
+ group.rb # Group model (Lutaml::Model) with predicate methods
31
+ group_collection.rb # Rich collection model (Enumerable, query methods, merge, from_file, save)
32
+ groups.yaml # Bundled group data (shipped in gem)
33
+ cli.rb # Thor CLI (fetch / integrate commands)
34
+ scrapers.rb # Scrapers.fetch_all / fetch_ietf / fetch_irtf → GroupCollections
35
+ scrapers/
36
+ base_scraper.rb # Abstract: fetch_html, log, build_group, build_collection
37
+ ietf_scraper.rb # Scrapes datatracker.ietf.org → GroupCollection
38
+ irtf_scraper.rb # Scrapes irtf.org → GroupCollection
39
+ ```
40
+
41
+ **Layer separation:**
42
+ - **Models** (`Group`, `GroupCollection`) — all data and query logic. GroupCollection includes Enumerable and supports chainable filters returning new GroupCollections (`collection.active.by_type("wg")`).
43
+ - **Facade** (`Importer` module) — loads bundled `groups.yaml` via `GroupCollection.from_file`, delegates query methods to the collection for backward-compatible API. Uses `autoload` (following sts-ruby pattern).
44
+ - **Scrapers** — each returns a `GroupCollection`. `Scrapers.fetch_all` uses `merge` to combine results. `BaseScraper` provides `build_group`/`build_collection` template methods.
45
+ - **CLI** (`Cli`) — thin Thor wrapper calling scraper and collection methods.
46
+
47
+ **Key design decisions:**
48
+ - Models use `lutaml-model` for serialization — attribute declarations + key_value mapping blocks, same as sts-ruby
49
+ - GroupCollection filter methods (`by_organization`, `by_type`, `by_area`, `active`, `concluded`) return new GroupCollections, enabling chaining (open/closed principle)
50
+ - All facade query methods return GroupCollection; only `groups` returns Array
51
+ - `Group` has predicate methods (`active?`, `ietf?`, `working_group?`) — business logic on the model, not in collection filters
52
+ - `GroupCollection.merge(other)` combines collections immutably
53
+ - `GroupCollection.from_file(path)` / `#save(path, format:)` handle persistence
54
+ - Entry point (`exe/ietf-data-importer`) requires the main importer file, triggering autoload — no direct requires in cli.rb
55
+ - Tests inject data by stubbing `Importer.collection` — no private state access or `class_variable_set`
56
+ - Scrapers are resilient to site layout changes: multiple CSS selectors tried as fallbacks
57
+ - Scraper methods return values rather than mutating parameters
58
+
59
+ ## Conventions
60
+
61
+ - Ruby 3.0+ required
62
+ - Frozen string literals everywhere
63
+ - Lutaml::Model for all data models (no raw Hash manipulation in the public API)
64
+ - Test fixtures in `spec/fixtures/`; specs in `spec/ietf/data/importer/` (group_spec, group_collection_spec, cli_spec, importer_spec)
65
+ - Tests stub `Importer.collection` scoped to `:query_tests` context — no global stubs, no private state access
66
+ - Rubocop config inherits from riboseinc/oss-guides (remote URL in `.rubocop.yml`)
67
+
68
+ ## Reference Project
69
+
70
+ The `sts-ruby` project at `../sts-ruby/` demonstrates the canonical patterns for Lutaml::Model-based gems in this org:
71
+ - `autoload` for lazy-loading modules/classes
72
+ - Each model is a single class in its own file, inheriting `Lutaml::Model::Serializable`
73
+ - One file per class; no `private` send or `respond_to?` — rely on typed interfaces
data/Gemfile CHANGED
@@ -5,7 +5,6 @@ source "https://rubygems.org"
5
5
  gemspec
6
6
 
7
7
  gem "rake"
8
-
9
8
  gem "rspec"
10
-
11
9
  gem "rubocop"
10
+ gem "rubocop-performance"
data/README.adoc CHANGED
@@ -7,7 +7,8 @@ image:https://img.shields.io/github/commits-since/metanorma/ietf-data-importer/l
7
7
 
8
8
  == Purpose
9
9
 
10
- IETF Data Importer is a Ruby gem providing access to information about IETF working groups and IRTF research groups.
10
+ IETF Data Importer is a Ruby gem providing access to information about IETF
11
+ working groups and IRTF research groups.
11
12
 
12
13
  It includes:
13
14
 
@@ -20,29 +21,6 @@ This gem exists because the official sources often change their layout or may be
20
21
  * https://datatracker.ietf.org/group/ (formerly tools.ietf.org/wg)
21
22
  * https://irtf.org/groups
22
23
 
23
- == Migration from metanorma-ietf-data
24
-
25
- This gem is the renamed and restructured version of `metanorma-ietf-data`. The namespace and file structure have been changed to match other Metanorma data importer gems.
26
-
27
- To migrate from metanorma-ietf-data:
28
-
29
- . Replace the following:
30
- +
31
- [source,diff]
32
- ----
33
- - require "metanorma/ietf/data"
34
- - groups = Metanorma::Ietf::Data.groups
35
- + require "ietf/data/importer"
36
- + groups = Ietf::Data::Importer.groups
37
- ----
38
-
39
- . Update your Gemfile:
40
- +
41
- [source,diff]
42
- ----
43
- - gem 'metanorma-ietf-data'
44
- + gem 'ietf-data-importer'
45
- ----
46
24
 
47
25
  == Installation
48
26
 
@@ -211,6 +189,36 @@ groups:
211
189
  ----
212
190
  ====
213
191
 
192
+ == Migration from metanorma-ietf-data
193
+
194
+ The versions 0.1.0 and 0.2.0 of this gem were published under the name
195
+ `metanorma-ietf-data`.
196
+
197
+ The gem was rewritten and republished as `ietf-data-importer` to better reflect
198
+ its purpose at version 0.3.0. The namespace and file structure have been changed
199
+ to match other Metanorma data importer gems.
200
+
201
+ To migrate from metanorma-ietf-data:
202
+
203
+ . Replace the following:
204
+ +
205
+ [source,diff]
206
+ ----
207
+ - require "metanorma/ietf/data"
208
+ - groups = Metanorma::Ietf::Data.groups
209
+ + require "ietf/data/importer"
210
+ + groups = Ietf::Data::Importer.groups
211
+ ----
212
+
213
+ . Update your Gemfile:
214
+ +
215
+ [source,diff]
216
+ ----
217
+ - gem 'metanorma-ietf-data'
218
+ + gem 'ietf-data-importer'
219
+ ----
220
+
221
+
214
222
  == Copyright
215
223
 
216
224
  This gem is developed, maintained and funded by https://www.ribose.com[Ribose Inc.]
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../lib/ietf/data/importer/cli"
4
+ require_relative "../lib/ietf/data/importer"
5
5
 
6
6
  Ietf::Data::Importer::Cli.start(ARGV)
@@ -21,9 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.license = "BSD-2-Clause"
22
22
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
23
23
 
24
- spec.add_dependency "lutaml-model", "~> 0.7"
24
+ spec.add_dependency "lutaml-model", "~> 0.8"
25
+ spec.add_dependency "nokogiri", "~> 1.19"
25
26
  spec.add_dependency "thor", "~> 1.0"
26
- spec.add_dependency "nokogiri", "~> 1.18"
27
27
  spec.add_dependency "yaml"
28
28
 
29
29
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
@@ -34,4 +34,5 @@ Gem::Specification.new do |spec|
34
34
  spec.bindir = "exe"
35
35
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
36
  spec.require_paths = ["lib"]
37
+ spec.metadata["rubygems_mfa_required"] = "true"
37
38
  end
@@ -1,45 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require "yaml"
5
4
  require "fileutils"
6
- require_relative "group_collection"
7
- require_relative "scrapers"
8
5
 
9
6
  module Ietf
10
7
  module Data
11
8
  module Importer
12
- # Command-line interface for IETF/IRTF group data
13
9
  class Cli < Thor
14
- desc "fetch OUTPUT_FILE", "Fetch IETF/IRTF groups and save to YAML file"
15
- option :format, type: :string, default: "yaml", desc: "Output format (yaml or json)"
10
+ desc "fetch OUTPUT_FILE", "Fetch IETF/IRTF groups and save to file"
11
+ option :format, type: :string, default: "yaml",
12
+ desc: "Output format (yaml or json)"
16
13
  def fetch(output_file = nil)
17
14
  output_file ||= "ietf_groups.#{options[:format]}"
18
15
 
19
- # Fetch all groups using the scrapers
20
- collection = Ietf::Data::Importer::Scrapers.fetch_all
16
+ collection = Scrapers.fetch_all
17
+ collection.save(output_file, format: options[:format].to_sym)
21
18
 
22
- # Save to file in the requested format
23
- format = options[:format].to_sym
24
- Ietf::Data::Importer::Scrapers.save_to_file(collection, output_file, format)
19
+ puts "Saved #{collection.size} groups to #{output_file}"
25
20
  end
26
21
 
27
22
  desc "integrate YAML_FILE", "Integrate YAML file as gem data"
28
23
  def integrate(yaml_file)
29
- # Validate YAML file
30
- begin
31
- collection = Ietf::Data::Importer::GroupCollection.from_yaml(File.read(yaml_file))
32
- rescue => e
33
- puts "Error reading YAML file: #{e.message}"
34
- exit 1
35
- end
24
+ collection = GroupCollection.from_yaml(File.read(yaml_file))
36
25
 
37
- # Save as YAML for gem usage
38
- target_yaml = File.join(File.dirname(__FILE__), "groups.yaml")
39
- FileUtils.mkdir_p(File.dirname(target_yaml))
40
- File.write(target_yaml, File.read(yaml_file))
26
+ target = File.join(__dir__, "groups.yaml")
27
+ FileUtils.mkdir_p(File.dirname(target))
28
+ File.write(target, File.read(yaml_file))
41
29
 
42
- puts "Integrated #{collection.groups.size} groups into gem"
30
+ puts "Integrated #{collection.size} groups into gem"
31
+ rescue StandardError => e
32
+ puts "Error reading YAML file: #{e.message}"
33
+ exit 1
43
34
  end
44
35
  end
45
36
  end
@@ -5,14 +5,17 @@ require "lutaml/model"
5
5
  module Ietf
6
6
  module Data
7
7
  module Importer
8
- # Represents a single IETF or IRTF group
9
8
  class Group < Lutaml::Model::Serializable
9
+ ORGANIZATIONS = %w[ietf irtf].freeze
10
+ STATUSES = %w[active concluded bof proposed].freeze
11
+ TYPES = %w[wg rg area team program dir ag bof].freeze
12
+
10
13
  attribute :abbreviation, :string
11
14
  attribute :name, :string
12
- attribute :organization, :string # 'ietf' or 'irtf'
13
- attribute :type, :string # 'wg', 'rg', etc.
15
+ attribute :organization, :string
16
+ attribute :type, :string
14
17
  attribute :area, :string
15
- attribute :status, :string, values: %w[active concluded bof proposed]
18
+ attribute :status, :string
16
19
  attribute :description, :string
17
20
  attribute :chairs, :string, collection: true
18
21
  attribute :mailing_list, :string
@@ -36,6 +39,38 @@ module Ietf
36
39
  map "charter_url", to: :charter_url
37
40
  map "concluded_date", to: :concluded_date
38
41
  end
42
+
43
+ def active?
44
+ status == "active"
45
+ end
46
+
47
+ def concluded?
48
+ status == "concluded"
49
+ end
50
+
51
+ def bof?
52
+ status == "bof"
53
+ end
54
+
55
+ def proposed?
56
+ status == "proposed"
57
+ end
58
+
59
+ def ietf?
60
+ organization == "ietf"
61
+ end
62
+
63
+ def irtf?
64
+ organization == "irtf"
65
+ end
66
+
67
+ def working_group?
68
+ type == "wg"
69
+ end
70
+
71
+ def research_group?
72
+ type == "rg"
73
+ end
39
74
  end
40
75
  end
41
76
  end
@@ -6,13 +6,113 @@ require_relative "group"
6
6
  module Ietf
7
7
  module Data
8
8
  module Importer
9
- # Represents a collection of IETF and IRTF groups
10
9
  class GroupCollection < Lutaml::Model::Serializable
10
+ include Enumerable
11
+
11
12
  attribute :groups, Group, collection: true
12
13
 
13
14
  key_value do
14
15
  map "groups", to: :groups
15
16
  end
17
+
18
+ def each(&block)
19
+ groups.each(&block)
20
+ end
21
+
22
+ def size
23
+ groups.size
24
+ end
25
+
26
+ def empty?
27
+ groups.empty?
28
+ end
29
+
30
+ def [](abbreviation)
31
+ find_by_abbreviation(abbreviation)
32
+ end
33
+
34
+ def find_by_abbreviation(abbreviation)
35
+ groups.find do |g|
36
+ g.abbreviation.downcase == abbreviation.to_s.downcase
37
+ end
38
+ end
39
+
40
+ def exists?(abbreviation)
41
+ !find_by_abbreviation(abbreviation).nil?
42
+ end
43
+
44
+ def by_organization(org)
45
+ self.class.new(groups: groups.select do |g|
46
+ g.organization == org.to_s
47
+ end)
48
+ end
49
+
50
+ def by_type(type)
51
+ self.class.new(groups: groups.select do |g|
52
+ g.type&.downcase == type.to_s.downcase
53
+ end)
54
+ end
55
+
56
+ def by_area(area)
57
+ self.class.new(groups: groups.select do |g|
58
+ g.area&.downcase == area.to_s.downcase
59
+ end)
60
+ end
61
+
62
+ def active
63
+ self.class.new(groups: groups.select(&:active?))
64
+ end
65
+
66
+ def concluded
67
+ self.class.new(groups: groups.select(&:concluded?))
68
+ end
69
+
70
+ def working_groups
71
+ by_type("wg")
72
+ end
73
+
74
+ def research_groups
75
+ by_type("rg")
76
+ end
77
+
78
+ def ietf_groups
79
+ by_organization("ietf")
80
+ end
81
+
82
+ def irtf_groups
83
+ by_organization("irtf")
84
+ end
85
+
86
+ def group_types
87
+ groups.filter_map(&:type).uniq.sort
88
+ end
89
+
90
+ def areas
91
+ groups.filter_map(&:area).uniq.sort
92
+ end
93
+
94
+ def merge(other)
95
+ self.class.new(groups: groups + other.groups)
96
+ end
97
+
98
+ def self.from_file(path, format: :yaml)
99
+ return new(groups: []) unless File.exist?(path)
100
+
101
+ content = File.read(path)
102
+ case format
103
+ when :yaml then from_yaml(content)
104
+ when :json then from_json(content)
105
+ else raise ArgumentError, "Unsupported format: #{format}"
106
+ end
107
+ end
108
+
109
+ def save(path, format: :yaml)
110
+ case format
111
+ when :yaml then File.write(path, to_yaml)
112
+ when :json then File.write(path, to_json)
113
+ else raise ArgumentError, "Unsupported format: #{format}"
114
+ end
115
+ end
16
116
  end
17
117
  end
18
118
  end
@@ -2,30 +2,39 @@
2
2
 
3
3
  require "nokogiri"
4
4
  require "open-uri"
5
+ require_relative "../group"
6
+ require_relative "../group_collection"
5
7
 
6
8
  module Ietf
7
9
  module Data
8
10
  module Importer
9
11
  module Scrapers
10
- # Base class for web scrapers
11
12
  class BaseScraper
12
- # Fetch HTML content from a URL and parse it with Nokogiri
13
- # @param url [String] The URL to fetch
14
- # @return [Nokogiri::HTML::Document] The parsed HTML document
13
+ def fetch
14
+ raise NotImplementedError, "#{self.class}#fetch must be implemented"
15
+ end
16
+
15
17
  def fetch_html(url)
16
18
  Nokogiri::HTML(URI.open(url))
17
- rescue => e
18
- puts " Error fetching URL #{url}: #{e.message}"
19
+ rescue StandardError => e
20
+ log "Error fetching URL #{url}: #{e.message}"
19
21
  nil
20
22
  end
21
23
 
22
- # Log a message with indentation
23
- # @param message [String] The message to log
24
- # @param level [Integer] The indentation level (default: 0)
25
24
  def log(message, level = 0)
26
25
  indent = " " * level
27
26
  puts "#{indent}#{message}"
28
27
  end
28
+
29
+ private
30
+
31
+ def build_group(attributes)
32
+ Group.new(attributes)
33
+ end
34
+
35
+ def build_collection(groups)
36
+ GroupCollection.new(groups: groups)
37
+ end
29
38
  end
30
39
  end
31
40
  end