ecfr 1.1.4 → 1.1.7

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -1
  3. data/CHANGELOG.md +18 -1
  4. data/Gemfile.lock +90 -66
  5. data/lib/ecfr/admin_service/base.rb +0 -1
  6. data/lib/ecfr/attribute_method_definition.rb +1 -4
  7. data/lib/ecfr/base.rb +5 -8
  8. data/lib/ecfr/client.rb +6 -2
  9. data/lib/ecfr/client_configuration.rb +122 -0
  10. data/lib/ecfr/configuration.rb +1 -0
  11. data/lib/ecfr/constants.rb +3 -1
  12. data/lib/ecfr/parallel_client.rb +10 -5
  13. data/lib/ecfr/renderer_service/origin.rb +1 -4
  14. data/lib/ecfr/search_service/base.rb +1 -0
  15. data/lib/ecfr/search_service/timeline.rb +29 -0
  16. data/lib/ecfr/testing/extensions/search_service/timeline_extensions.rb +13 -0
  17. data/lib/ecfr/testing/extensions/versioner_service/reference_extensions.rb +15 -0
  18. data/lib/ecfr/testing/extensions/versioner_service/topic_extensions.rb +16 -0
  19. data/lib/ecfr/testing/factories/search_service/timeline_factory.rb +9 -0
  20. data/lib/ecfr/testing/factories/versioner_service/reference_factory.rb +16 -0
  21. data/lib/ecfr/testing/factories/versioner_service/topic_factory.rb +34 -0
  22. data/lib/ecfr/varnish_cache_service/base.rb +7 -1
  23. data/lib/ecfr/version.rb +1 -1
  24. data/lib/ecfr/versioner_service/agency.rb +9 -0
  25. data/lib/ecfr/versioner_service/ancestors.rb +1 -4
  26. data/lib/ecfr/versioner_service/authority.rb +48 -0
  27. data/lib/ecfr/versioner_service/base.rb +4 -0
  28. data/lib/ecfr/versioner_service/reference.rb +19 -0
  29. data/lib/ecfr/versioner_service/title.rb +0 -2
  30. data/lib/ecfr/versioner_service/topic.rb +74 -0
  31. data/lib/ecfr/versioner_service/xml_content.rb +0 -2
  32. data/lib/ecfr.rb +3 -0
  33. metadata +14 -3
  34. data/lib/ecfr/admin_service/build.rb +0 -38
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93cb578d4fc85f48e9d1d0ac423522886fa575a2425da6836547eb075d4e6cf5
4
- data.tar.gz: b896739d0023e6398da9effbab99ed0b48323d6a61b9f21b94b2680f9ed74146
3
+ metadata.gz: 13cc40dfeeb370c7a81be66f37bdf9f23cf4dfbb936465ce4c12b1ab88094deb
4
+ data.tar.gz: 6a80785bbd1e428f93f841401e678c1776f9b344a33e554738759937079eca06
5
5
  SHA512:
6
- metadata.gz: cc03bfa1d18e5e202f4e3f069d4cf3f8ee3caf5aa3af26fdf0f9813a1c9c7756fe403764ac69f00268e3c96cbad85f1697e69eb8eab6f74b4d84351e74806e2e
7
- data.tar.gz: 599a3c76236594b98fde583b801922b3bfb686071c9cbf2460610895d229580c428ba02026898b181dd0729dcfa18ccbecd8c1af5fde036edc17229c55ca739d
6
+ metadata.gz: 11198b0d371f4c2a49ed939af2db6f7b781809d107b1e17ef586b03676d21157f739b92e4ae681fe70112580650a7476b39cbdb0ea6e06c51067cc79943510dd
7
+ data.tar.gz: 3dddd1f095d6ea3015e8d4f9dfbb0d1fe9ab29cfd426274c6d646fa86bc02217c45545d2105682cf0e88ee33779b22f344c895feab0c55e2be0d036c9873332a
data/.rubocop.yml CHANGED
@@ -18,9 +18,11 @@ inherit_mode:
18
18
  merge:
19
19
  - Exclude
20
20
 
21
- require:
21
+ plugins:
22
22
  # Performance cops are bundled with Standard
23
23
  - rubocop-performance
24
+
25
+ require:
24
26
  # Standard's config uses this custom cop,
25
27
  # so it must be loaded
26
28
  - standard/cop/block_single_line_braces
data/CHANGELOG.md CHANGED
@@ -1,8 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.7] - 2026-06-29
4
+ ### Additions
5
+ - Add authority endpoint
6
+ - Add topic endpoint
7
+ - Support for singular and plural change types (4552a8d5)
8
+
9
+ ## [1.1.6] - 2025-09-23
10
+ ### Fixes
11
+ - Add missing Gemfile lock changes
12
+ - See 1.1.5
13
+
14
+ ## [1.1.5] - 2025-09-23 - yanked
15
+ ### Additions
16
+ - Add timeline endpoint (8490dbf1)
17
+ - Removed puts from client to cleanup unwanted output (31846f86)
18
+ - Add support for using 'delete' verb when attempting to clear varnish cache. (7d7a5870)
19
+
3
20
  ## [1.1.4] - 2025-03-27
4
21
  ### Additions
5
- - Add factory for Agency (38185411)
22
+ - Add factory for Agency (38185411)
6
23
  ### Dependencies
7
24
  - Removed lock to Rails 6 (f6c5024a)
8
25
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ecfr (1.1.3)
4
+ ecfr (1.1.6)
5
5
  activemodel
6
6
  activesupport
7
7
  faraday (~> 2.0)
@@ -11,133 +11,157 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activemodel (7.2.1)
15
- activesupport (= 7.2.1)
16
- activesupport (7.2.1)
14
+ activemodel (8.1.2)
15
+ activesupport (= 8.1.2)
16
+ activesupport (8.1.2)
17
17
  base64
18
18
  bigdecimal
19
19
  concurrent-ruby (~> 1.0, >= 1.3.1)
20
20
  connection_pool (>= 2.2.5)
21
21
  drb
22
22
  i18n (>= 1.6, < 2)
23
+ json
23
24
  logger (>= 1.4.2)
24
25
  minitest (>= 5.1)
25
26
  securerandom (>= 0.3)
26
27
  tzinfo (~> 2.0, >= 2.0.5)
27
- ast (2.4.2)
28
- base64 (0.2.0)
29
- bigdecimal (3.1.8)
28
+ uri (>= 0.13.1)
29
+ ast (2.4.3)
30
+ base64 (0.3.0)
31
+ bigdecimal (4.0.1)
30
32
  coderay (1.1.3)
31
- concurrent-ruby (1.3.4)
32
- connection_pool (2.4.1)
33
- diff-lcs (1.5.1)
34
- drb (2.2.1)
35
- ethon (0.16.0)
33
+ concurrent-ruby (1.3.6)
34
+ connection_pool (3.0.2)
35
+ diff-lcs (1.6.2)
36
+ drb (2.2.3)
37
+ ethon (0.18.0)
36
38
  ffi (>= 1.15.0)
37
- factory_bot (6.5.0)
38
- activesupport (>= 5.0.0)
39
- faraday (2.12.0)
40
- faraday-net_http (>= 2.0, < 3.4)
39
+ logger
40
+ factory_bot (6.5.6)
41
+ activesupport (>= 6.1.0)
42
+ faraday (2.14.0)
43
+ faraday-net_http (>= 2.0, < 3.5)
41
44
  json
42
45
  logger
43
- faraday-net_http (3.3.0)
44
- net-http
45
- faraday-net_http_persistent (2.3.0)
46
+ faraday-net_http (3.4.2)
47
+ net-http (~> 0.5)
48
+ faraday-net_http_persistent (2.3.1)
46
49
  faraday (~> 2.5)
47
50
  net-http-persistent (>= 4.0.4, < 5)
48
51
  faraday-typhoeus (1.1.0)
49
52
  faraday (~> 2.0)
50
53
  typhoeus (~> 1.4)
51
- ffi (1.17.0)
52
- i18n (1.14.6)
54
+ ffi (1.17.3-arm64-darwin)
55
+ ffi (1.17.3-x86_64-darwin)
56
+ ffi (1.17.3-x86_64-linux-gnu)
57
+ ffi (1.17.3-x86_64-linux-musl)
58
+ i18n (1.14.8)
53
59
  concurrent-ruby (~> 1.0)
54
- json (2.7.2)
55
- language_server-protocol (3.17.0.3)
60
+ io-console (0.8.2)
61
+ json (2.18.0)
62
+ language_server-protocol (3.17.0.5)
56
63
  lint_roller (1.1.0)
57
- logger (1.6.1)
64
+ logger (1.7.0)
58
65
  method_source (1.1.0)
59
- mini_portile2 (2.8.7)
60
- minitest (5.25.1)
61
- net-http (0.4.1)
62
- uri
63
- net-http-persistent (4.0.4)
64
- connection_pool (~> 2.2)
65
- nokogiri (1.16.7)
66
- mini_portile2 (~> 2.8.2)
66
+ minitest (6.0.1)
67
+ prism (~> 1.5)
68
+ net-http (0.9.1)
69
+ uri (>= 0.11.1)
70
+ net-http-persistent (4.0.8)
71
+ connection_pool (>= 2.2.4, < 4)
72
+ nokogiri (1.19.0-arm64-darwin)
73
+ racc (~> 1.4)
74
+ nokogiri (1.19.0-x86_64-darwin)
75
+ racc (~> 1.4)
76
+ nokogiri (1.19.0-x86_64-linux-gnu)
67
77
  racc (~> 1.4)
68
- ostruct (0.6.1)
69
- parallel (1.26.3)
70
- parallel_tests (4.7.2)
78
+ nokogiri (1.19.0-x86_64-linux-musl)
79
+ racc (~> 1.4)
80
+ ostruct (0.6.3)
81
+ parallel (1.27.0)
82
+ parallel_tests (4.10.1)
71
83
  parallel
72
- parser (3.3.5.0)
84
+ parser (3.3.10.1)
73
85
  ast (~> 2.4.1)
74
86
  racc
75
- pry (0.14.2)
87
+ prism (1.9.0)
88
+ pry (0.16.0)
76
89
  coderay (~> 1.1)
77
90
  method_source (~> 1.0)
91
+ reline (>= 0.6.0)
78
92
  racc (1.8.1)
79
- rack (3.1.7)
93
+ rack (3.2.4)
80
94
  rainbow (3.1.1)
81
- regexp_parser (2.9.2)
95
+ regexp_parser (2.11.3)
96
+ reline (0.6.3)
97
+ io-console (~> 0.5)
82
98
  request_store (1.7.0)
83
99
  rack (>= 1.4)
84
- rspec (3.13.0)
100
+ rspec (3.13.2)
85
101
  rspec-core (~> 3.13.0)
86
102
  rspec-expectations (~> 3.13.0)
87
103
  rspec-mocks (~> 3.13.0)
88
- rspec-core (3.13.1)
104
+ rspec-core (3.13.6)
89
105
  rspec-support (~> 3.13.0)
90
- rspec-expectations (3.13.3)
106
+ rspec-expectations (3.13.5)
91
107
  diff-lcs (>= 1.2.0, < 2.0)
92
108
  rspec-support (~> 3.13.0)
93
- rspec-mocks (3.13.2)
109
+ rspec-mocks (3.13.7)
94
110
  diff-lcs (>= 1.2.0, < 2.0)
95
111
  rspec-support (~> 3.13.0)
96
- rspec-support (3.13.1)
112
+ rspec-support (3.13.7)
97
113
  rspec_junit_formatter (0.6.0)
98
114
  rspec-core (>= 2, < 4, != 2.12.0)
99
- rubocop (1.66.1)
115
+ rubocop (1.82.1)
100
116
  json (~> 2.3)
101
- language_server-protocol (>= 3.17.0)
117
+ language_server-protocol (~> 3.17.0.2)
118
+ lint_roller (~> 1.1.0)
102
119
  parallel (~> 1.10)
103
120
  parser (>= 3.3.0.2)
104
121
  rainbow (>= 2.2.2, < 4.0)
105
- regexp_parser (>= 2.4, < 3.0)
106
- rubocop-ast (>= 1.32.2, < 2.0)
122
+ regexp_parser (>= 2.9.3, < 3.0)
123
+ rubocop-ast (>= 1.48.0, < 2.0)
107
124
  ruby-progressbar (~> 1.7)
108
- unicode-display_width (>= 2.4.0, < 3.0)
109
- rubocop-ast (1.32.3)
110
- parser (>= 3.3.1.0)
111
- rubocop-performance (1.22.1)
112
- rubocop (>= 1.48.1, < 2.0)
113
- rubocop-ast (>= 1.31.1, < 2.0)
125
+ unicode-display_width (>= 2.4.0, < 4.0)
126
+ rubocop-ast (1.49.0)
127
+ parser (>= 3.3.7.2)
128
+ prism (~> 1.7)
129
+ rubocop-performance (1.26.1)
130
+ lint_roller (~> 1.1)
131
+ rubocop (>= 1.75.0, < 2.0)
132
+ rubocop-ast (>= 1.47.1, < 2.0)
114
133
  ruby-progressbar (1.13.0)
115
- securerandom (0.3.1)
116
- standard (1.41.0)
134
+ securerandom (0.4.1)
135
+ standard (1.53.0)
117
136
  language_server-protocol (~> 3.17.0.2)
118
137
  lint_roller (~> 1.0)
119
- rubocop (~> 1.66.0)
138
+ rubocop (~> 1.82.0)
120
139
  standard-custom (~> 1.0.0)
121
- standard-performance (~> 1.5)
140
+ standard-performance (~> 1.8)
122
141
  standard-custom (1.0.2)
123
142
  lint_roller (~> 1.0)
124
143
  rubocop (~> 1.50)
125
- standard-performance (1.5.0)
144
+ standard-performance (1.9.0)
126
145
  lint_roller (~> 1.1)
127
- rubocop-performance (~> 1.22.0)
128
- turbo_tests (2.2.4)
146
+ rubocop-performance (~> 1.26.0)
147
+ turbo_tests (2.2.5)
129
148
  parallel_tests (>= 3.3.0, < 5)
130
149
  rspec (>= 3.10)
131
150
  typhoeus (1.4.1)
132
151
  ethon (>= 0.9.0)
133
152
  tzinfo (2.0.6)
134
153
  concurrent-ruby (~> 1.0)
135
- unicode-display_width (2.6.0)
136
- uri (0.13.1)
137
- yard (0.9.37)
154
+ unicode-display_width (3.2.0)
155
+ unicode-emoji (~> 4.1)
156
+ unicode-emoji (4.2.0)
157
+ uri (1.1.1)
158
+ yard (0.9.38)
138
159
 
139
160
  PLATFORMS
140
- ruby
161
+ arm64-darwin
162
+ x86_64-darwin
163
+ x86_64-linux-gnu
164
+ x86_64-linux-musl
141
165
 
142
166
  DEPENDENCIES
143
167
  ecfr!
@@ -155,4 +179,4 @@ DEPENDENCIES
155
179
  yard
156
180
 
157
181
  BUNDLED WITH
158
- 2.6.5
182
+ 2.6.7
@@ -5,7 +5,6 @@ module Ecfr
5
5
  require_relative "status"
6
6
 
7
7
  require_relative "agency"
8
- require_relative "build"
9
8
  require_relative "ecfr_correction"
10
9
  require_relative "editorial_note"
11
10
  require_relative "ibr_cfr_range"
@@ -43,10 +43,7 @@ module Ecfr
43
43
  end
44
44
 
45
45
  if options[:type] == :boolean
46
- define_method :"#{attr}?" do
47
- val = extract_value(attr, options)
48
- Ecfr::AttributeCaster.cast_attr(val, options[:type], options[:options] || {})
49
- end
46
+ alias_method :"#{attr}?", :"#{attr}"
50
47
  end
51
48
  end
52
49
  end
data/lib/ecfr/base.rb CHANGED
@@ -31,13 +31,14 @@ module Ecfr
31
31
 
32
32
  attr_reader :metadata, :results, :response_status, :request_data
33
33
 
34
- SUPPORTED_ARRAY_ACCESSORS = %i[empty? first last size]
35
- delegate(*SUPPORTED_ARRAY_ACCESSORS, to: :results)
34
+ SUPPORTED_ARRAY_ACCESSORS = %i[each empty? first last size]
35
+ delegate(*SUPPORTED_ARRAY_ACCESSORS, to: :results, allow_nil: true)
36
36
  alias_method :all, :results
37
37
 
38
+ DEFAULT_OPTIONS = {base: true}
39
+
38
40
  def initialize(attributes = {}, options = {})
39
- default_options = {base: true}
40
- options = default_options.merge(options)
41
+ options.reverse_merge!(DEFAULT_OPTIONS)
41
42
 
42
43
  @response_status = options.delete(:response_status)
43
44
  @request_data = options.delete(:request_data)
@@ -58,10 +59,6 @@ module Ecfr
58
59
  super(attributes)
59
60
  end
60
61
 
61
- def each
62
- @results.each { |result| yield result }
63
- end
64
-
65
62
  class Metadata
66
63
  include AttributeMethodDefinition
67
64
 
data/lib/ecfr/client.rb CHANGED
@@ -142,7 +142,8 @@ module Ecfr
142
142
 
143
143
  response = instrument("Ecfr::Perform #{cache_key.to_s.tr("\"", "'")}") do
144
144
  RequestStore.fetch(cache_key) do
145
- puts "Request not in eCFR gem cache, fetching..."
145
+ # TODO: replace this puts with a logger
146
+ # puts "Request not in eCFR gem cache, fetching..."
146
147
 
147
148
  response = fetch(method, path, params: params,
148
149
  client_options: client_options)
@@ -201,7 +202,6 @@ module Ecfr
201
202
  #
202
203
  def self.build(response:,
203
204
  request_data: {}, build_options: {})
204
-
205
205
  default_build_options = {parse_response: true}
206
206
  build_options = default_build_options.merge(build_options)
207
207
 
@@ -236,6 +236,7 @@ module Ecfr
236
236
 
237
237
  execute do
238
238
  c.get(path, params) do |req|
239
+ yield(req) if block_given?
239
240
  Ecfr.config.request_hook.call(req)
240
241
  end
241
242
  end
@@ -246,6 +247,7 @@ module Ecfr
246
247
 
247
248
  execute do
248
249
  c.post(path, params) do |req|
250
+ yield(req) if block_given?
249
251
  Ecfr.config.request_hook.call(req)
250
252
  end
251
253
  end
@@ -256,6 +258,7 @@ module Ecfr
256
258
 
257
259
  execute do
258
260
  c.delete(path, params) do |req|
261
+ yield(req) if block_given?
259
262
  Ecfr.config.request_hook.call(req)
260
263
  end
261
264
  end
@@ -268,6 +271,7 @@ module Ecfr
268
271
  execute do
269
272
  c.run_request(:purge, path, nil, nil) do |request|
270
273
  request.params.update(params)
274
+ yield(req) if block_given?
271
275
  Ecfr.config.request_hook.call(request)
272
276
  end
273
277
  end
@@ -0,0 +1,122 @@
1
+ module Ecfr
2
+ #
3
+ # Configures the Ecfr gem from a consuming application's settings and
4
+ # credentials. Lives in the gem (rather than each app) so the contract
5
+ # between app config and the gem is defined and validated in one place.
6
+ #
7
+ # Everything the gem can't assume exists is passed in explicitly:
8
+ # - settings: the app's Settings object (duck-typed; dotted/[] access)
9
+ # - env: the app environment, used in the user_agent string
10
+ # - credentials: optional; anything responding to #dig (e.g. Rails credentials)
11
+ #
12
+ # An optional block is yielded the config last, after every settings-derived
13
+ # assignment, so the app can override any value (request hooks, timeouts,
14
+ # urls, ...) without the gem depending on app-level constants.
15
+ #
16
+ class ClientConfiguration
17
+ class InvalidSettings < StandardError; end
18
+ class InvalidCredentials < StandardError; end
19
+
20
+ # Settings with no gem-side default must be present. Everything else has a
21
+ # default in Ecfr::Configuration::CONFIG_DEFAULTS and is therefore optional.
22
+ REQUIRED_SETTINGS = [
23
+ %i[container process],
24
+ %i[container role],
25
+ %i[container hostname]
26
+ ].freeze
27
+
28
+ def self.initialize_for(service, settings, env, credentials = nil)
29
+ validate_settings!(settings)
30
+ validate_credentials!(credentials)
31
+
32
+ Ecfr.configure do |config|
33
+ config.user_agent = [
34
+ "ecfr", service, env,
35
+ dig_setting(settings, :container, :process),
36
+ dig_setting(settings, :container, :role),
37
+ dig_setting(settings, :container, :hostname)
38
+ ].join("-")
39
+
40
+ log_http_requests = dig_setting(settings, :services, :ecfr, :log_http_requests)
41
+ config.log_http_requests = log_http_requests unless log_http_requests.nil?
42
+
43
+ cache_responses = dig_setting(settings, :services, :ecfr, :cache_responses)
44
+ config.cache_responses = cache_responses unless cache_responses.nil?
45
+
46
+ # prefer internal urls when setting up gem
47
+ base_url = dig_setting(settings, :services, :ecfr, :internal_base_url) ||
48
+ dig_setting(settings, :services, :ecfr, :base_url)
49
+ config.base_url = base_url if base_url.present?
50
+
51
+ profile_base_url = dig_setting(settings, :services, :ofr, :profile, :internal_base_url) ||
52
+ dig_setting(settings, :services, :ofr, :profile, :base_url)
53
+ config.ofr_profile_service_base_url = profile_base_url if profile_base_url.present?
54
+
55
+ profile_path = dig_setting(settings, :services, :ofr, :profile, :path)
56
+ config.ofr_profile_service_path = profile_path if profile_path.present?
57
+
58
+ # configure services according to settings
59
+ service_keys = Ecfr.services.filter_map do |service_module|
60
+ # ofr profile is configured separately above
61
+ next if service_module::Base.service_name == "OFR Profile"
62
+
63
+ service_module::Base.service_name.downcase.tr(" ", "_")
64
+ end
65
+
66
+ service_keys.each do |service_key|
67
+ url = dig_setting(settings, :services, :ecfr, service_key, :url)
68
+ config.send(:"#{service_key}_url=", url) if url.present?
69
+
70
+ path = dig_setting(settings, :services, :ecfr, service_key, :path)
71
+ config.send(:"#{service_key}_path=", path) if path.present?
72
+ end
73
+
74
+ open_timeout = dig_setting(settings, :services, :ecfr, :open_timeout)
75
+ config.open_timeout = open_timeout unless open_timeout.nil?
76
+
77
+ pdf_timeout = dig_setting(settings, :app, :timeouts, :pdf_timeout)
78
+ config.prince_xml_service_pdf_timeout = pdf_timeout unless pdf_timeout.nil?
79
+
80
+ # basic auth - some endpoints require auth (optional)
81
+ config.ecfr_basic_auth_username = credentials&.dig(:services, :ecfr, :http_basic, :username)
82
+ config.ecfr_basic_auth_password = credentials&.dig(:services, :ecfr, :http_basic, :password)
83
+
84
+ # let the app override any of the above (request hooks, urls, timeouts)
85
+ yield config if block_given?
86
+ end
87
+ end
88
+
89
+ def self.validate_settings!(settings)
90
+ raise InvalidSettings, "settings is required" if settings.nil?
91
+
92
+ missing = REQUIRED_SETTINGS.select { |path| dig_setting(settings, *path).blank? }
93
+ return if missing.empty?
94
+
95
+ raise InvalidSettings,
96
+ "missing required setting(s): #{missing.map { |path| path.join(".") }.join(", ")}"
97
+ end
98
+
99
+ def self.validate_credentials!(credentials)
100
+ return if credentials.nil?
101
+ return if credentials.respond_to?(:dig)
102
+
103
+ raise InvalidCredentials, "credentials must respond to #dig (got #{credentials.class})"
104
+ end
105
+
106
+ # Safely walk a settings tree via method or [] access, returning nil if any
107
+ # segment is missing rather than raising NoMethodError.
108
+ def self.dig_setting(node, *path)
109
+ path.reduce(node) do |obj, key|
110
+ break nil if obj.nil?
111
+
112
+ if obj.respond_to?(key)
113
+ obj.public_send(key)
114
+ elsif obj.respond_to?(:[])
115
+ obj[key]
116
+ end
117
+ end
118
+ end
119
+
120
+ private_class_method :validate_settings!, :validate_credentials!, :dig_setting
121
+ end
122
+ end
@@ -26,6 +26,7 @@ module Ecfr
26
26
  subscriptions_service_path: nil,
27
27
  varnish_cache_service_url: nil,
28
28
  varnish_cache_service_path: nil,
29
+ varnish_cache_service_clear_method: "purge",
29
30
  versioner_service_url: nil,
30
31
  versioner_service_path: nil,
31
32
  # basic auth - some endpoints require auth
@@ -2,13 +2,15 @@ module Ecfr
2
2
  module Constants
3
3
  module ChangeTypes
4
4
  KNOWN_CHANGE_TYPES = {
5
+ cross_reference: "Regulations linked from the above-dated Federal Register:",
5
6
  cross_references: "Regulations linked from the above-dated Federal Register:",
6
7
  delayed: "Regulations delayed in the above-dated Federal Register:",
7
8
  delayed_withdrawn: "Regulations delayed or withdrawn in the above-dated Federal Register:",
8
9
  delayed_withdrawn_extended: "Regulations delayed, withdrawn, or extended in the above-dated Federal Register:",
9
10
  effective: "Effective regulations inserted from the above-dated Federal Register:",
11
+ effective_cross_reference: "Regulations inserted that were previously linked and became effective on the date listed above:",
10
12
  effective_cross_references: "Regulations inserted that were previously linked and became effective on the date listed above:",
11
- expired: "Effective dates that expire on this date:",
13
+ expired: "Effective regulations that expire on this date:",
12
14
  extended: "Regulations extended in the above-dated Federal Register",
13
15
  initial: "Initial import of this content - change type is indeterminate"
14
16
  }
@@ -1,24 +1,29 @@
1
1
  module Ecfr
2
2
  module ParallelClient
3
3
  module ClassMethods
4
- def parallel_client(base_url:, client_options: {})
4
+ def parallel_client(base_url: self.base_url, client_options: {})
5
5
  client(
6
6
  base_url: base_url,
7
7
  client_options: client_options.merge({adapter: :typhoeus})
8
8
  )
9
9
  end
10
10
 
11
- # currently only handles expected cases when calling -renderer
12
- def parallel_get(requests, client)
11
+ def parallel(method, requests, client = parallel_client)
13
12
  client.in_parallel do
14
13
  requests.each do |request|
15
- request.response = client.get(request.path, request.args) do |req|
14
+ request.response = client.send(method, request.path, request.args) do |req|
16
15
  Ecfr.config.request_hook.call(req)
17
16
  end
18
17
  end
19
18
  end
19
+ end
20
+
21
+ def parallel_get(requests, client = parallel_client)
22
+ parallel(:get, requests, client)
23
+ end
20
24
 
21
- requests
25
+ def parallel_post(requests, client = parallel_client)
26
+ parallel(:post, requests, client)
22
27
  end
23
28
  end
24
29
 
@@ -40,10 +40,7 @@ module Ecfr
40
40
  ORIGIN_PATH = "v1/origin"
41
41
 
42
42
  def self.find(date, title_number, params = {})
43
- supported_params = (
44
- Ecfr::Constants::Hierarchy::HIERARCHY_LEVELS[1, 8] +
45
- [:build_id]
46
- ).map(&:to_sym)
43
+ supported_params = Ecfr::Constants::Hierarchy::HIERARCHY_LEVELS[1, 8].map(&:to_sym)
47
44
 
48
45
  perform(
49
46
  :get,
@@ -7,6 +7,7 @@ module Ecfr
7
7
  require_relative "content_version"
8
8
  require_relative "facet_base"
9
9
  require_relative "date_facet"
10
+ require_relative "timeline"
10
11
  require_relative "title_facet"
11
12
 
12
13
  def self.base_url
@@ -0,0 +1,29 @@
1
+ module Ecfr
2
+ module SearchService
3
+ class Timeline < FacetBase
4
+ include AttributeMethodDefinition
5
+
6
+ class Entry < Base
7
+ include AttributeMethodDefinition
8
+
9
+ def initialize(attributes = {}, options = {})
10
+ date, attributes, _ = attributes
11
+ attributes["date"] = date
12
+ attributes["change_types"].compact!
13
+ super
14
+ end
15
+
16
+ attribute :change_types, type: Array(:symbol)
17
+ attribute :count, type: :integer
18
+ attribute :date, type: :date, as: :key
19
+ end
20
+
21
+ attribute :timeline, type: Array(Entry)
22
+
23
+ # @return [<Timeline::Entry>]
24
+ def self.search(options = {})
25
+ super(:timeline, options)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ class Ecfr::SearchService::Timeline
2
+ extend ResponseHelper
3
+
4
+ def self.response_for(timeline)
5
+ results = {
6
+ timeline: timeline
7
+ }.to_json
8
+
9
+ build(
10
+ response: stubbed_response(results)
11
+ )
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class Ecfr::VersionerService::Reference
2
+ extend ResponseHelper
3
+
4
+ def self.response_for(references)
5
+ references = references.is_a?(Array) ? references : [references]
6
+
7
+ results = {
8
+ references: references
9
+ }.compact
10
+
11
+ build(
12
+ response: stubbed_response(results.to_json)
13
+ )
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class Ecfr::VersionerService::Topic
2
+ extend ResponseHelper
3
+
4
+ def self.response_for(topics, meta: {})
5
+ topics = topics.is_a?(Array) ? topics : [topics]
6
+
7
+ results = {
8
+ topics: topics,
9
+ meta: meta
10
+ }.compact
11
+
12
+ build(
13
+ response: stubbed_response(results.to_json)
14
+ )
15
+ end
16
+ end
@@ -0,0 +1,9 @@
1
+ FactoryBot.define do
2
+ factory :timeline, class: "Ecfr::SearchService::Timeline" do
3
+ skip_create
4
+
5
+ initialize_with {
6
+ new(attributes.deep_stringify_keys)
7
+ }
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ FactoryBot.define do
2
+ factory :versioner_service_reference, class: "Ecfr::VersionerService::Reference" do
3
+ skip_create
4
+
5
+ reference { "1 CFR 1" }
6
+ description { "Definitions" }
7
+
8
+ initialize_with do
9
+ new(attributes.deep_stringify_keys)
10
+ end
11
+
12
+ trait :minimal do
13
+ description { "" }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,34 @@
1
+ FactoryBot.define do
2
+ factory :topic, class: "Ecfr::VersionerService::Topic" do
3
+ skip_create
4
+
5
+ name { "Sample Topic" }
6
+ references { ["1 CFR 1", "1 CFR 2"] }
7
+ see_also { ["Related Topic"] }
8
+ see { [] }
9
+
10
+ initialize_with do
11
+ new(attributes.deep_stringify_keys)
12
+ end
13
+
14
+ trait :with_references do
15
+ references { ["1 CFR 1", "1 CFR 2", "1 CFR 3"] }
16
+ end
17
+
18
+ trait :with_see_also do
19
+ see_also { ["Related Topic 1", "Related Topic 2"] }
20
+ end
21
+
22
+ trait :with_see do
23
+ see { ["Redirect Topic"] }
24
+ references { [] }
25
+ see_also { [] }
26
+ end
27
+
28
+ trait :minimal do
29
+ references { [] }
30
+ see_also { [] }
31
+ see { [] }
32
+ end
33
+ end
34
+ end
@@ -14,7 +14,13 @@ module Ecfr
14
14
  end
15
15
 
16
16
  def self.expire(path)
17
- purge(path)
17
+ # not all backends support the purge verb
18
+ # - the varnish purge proxy expects delete
19
+ if Ecfr.config.varnish_cache_service_clear_method.to_s == "delete"
20
+ delete(path)
21
+ else
22
+ purge(path)
23
+ end
18
24
  end
19
25
 
20
26
  def self.expire_everything
data/lib/ecfr/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ecfr
4
- VERSION = "1.1.4"
4
+ VERSION = "1.1.7"
5
5
  end
@@ -0,0 +1,9 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class Agency < Ecfr::AdminService::Agency
4
+ attribute :references,
5
+ type: Array(:string),
6
+ desc: "array of CFR references for this topic"
7
+ end
8
+ end
9
+ end
@@ -65,8 +65,6 @@ module Ecfr
65
65
  # hierarchy of the requested content
66
66
  # @option options [<Integer>] :descendant_depth the number of
67
67
  # levels of descendents to include in the structure
68
- # @option options [<Integer>] :build_id internal use only - used to
69
- # retreive data about a specific build
70
68
  # @option options [<Boolean>] :metadata whether to include metadata in
71
69
  # the response
72
70
  # @option options [<Integer>] :max_items
@@ -80,8 +78,7 @@ module Ecfr
80
78
  hierarchy_args = hierarchy_args.symbolize_keys
81
79
  options = options.symbolize_keys
82
80
 
83
- options = hierarchy_args.merge(options).except(:title, :metadata)
84
- options[:metadata] = true
81
+ options = hierarchy_args.merge(options).except(:title)
85
82
 
86
83
  perform(
87
84
  :get,
@@ -0,0 +1,48 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ #
4
+ # Authority entries represent individual references in the
5
+ # Parallel Table of Authorities.
6
+ #
7
+ class Authority < Base
8
+ result_key :authorities
9
+
10
+ attribute :kind,
11
+ desc: "authority reference kind"
12
+
13
+ attribute :components,
14
+ desc: "authority citation components"
15
+
16
+ attribute :date,
17
+ type: :date,
18
+ desc: "authority citation date"
19
+
20
+ attribute :references,
21
+ desc: "CFR titles, parts, and hierarchies that cite this authority"
22
+
23
+ AUTHORITIES_PATH = "v1/authorities"
24
+
25
+ metadata_key :meta
26
+
27
+ metadata :categories,
28
+ desc: "reference categories included in this response"
29
+
30
+ #
31
+ # Retrieves the Parallel Table of Authorities data.
32
+ #
33
+ # @param [<Hash>] options
34
+ # @option options [String] :year ("current") the year or 'current' to retrieve authorities for
35
+ #
36
+ # @return [Authority] grouped authority data and metadata
37
+ #
38
+ def self.all(options = {})
39
+ year = options.fetch(:year, "current")
40
+
41
+ perform(
42
+ :get,
43
+ "#{AUTHORITIES_PATH}/#{year}"
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
@@ -6,9 +6,13 @@ module Ecfr
6
6
 
7
7
  # note: structure must be required before ancestors
8
8
  require_relative "structure"
9
+ require_relative "agency"
9
10
  require_relative "ancestors"
11
+ require_relative "authority"
10
12
  require_relative "issue_package"
11
13
  require_relative "title"
14
+ require_relative "reference"
15
+ require_relative "topic"
12
16
  require_relative "xml_content"
13
17
 
14
18
  def self.base_url
@@ -0,0 +1,19 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ #
4
+ # References in the topics endpoint provide detailed information
5
+ # about CFR references mentioned in topics and agencies.
6
+ #
7
+ # Each reference contains a CFR reference string and a description.
8
+ #
9
+ class Reference < Base
10
+ result_key :references
11
+
12
+ attribute :reference,
13
+ desc: "CFR reference ('1 CFR 1')"
14
+
15
+ attribute :description,
16
+ desc: "description of the reference"
17
+ end
18
+ end
19
+ end
@@ -61,8 +61,6 @@ module Ecfr
61
61
  # Retreive the list of all Titles
62
62
  #
63
63
  # @param [<Hash>] options
64
- # @option options [String] :build_id internal use only - used
65
- # to retreive data about a specific build
66
64
  #
67
65
  # @return [<Title>] array of Titles with data
68
66
  #
@@ -0,0 +1,74 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ #
4
+ # Topics represent entries in the CFR Index and Finding Aids.
5
+ #
6
+ # Each topic contains a name and may have references to specific
7
+ # CFR sections, see_also references to other topics, or see
8
+ # references for redirects.
9
+ #
10
+ class Topic < Base
11
+ result_key :topics
12
+
13
+ attribute :adhoc,
14
+ type: :boolean,
15
+ desc: "distinguish informal topics from thesaurus entries"
16
+
17
+ attribute :name,
18
+ desc: "name of the topic"
19
+
20
+ attribute :references,
21
+ type: Array(:string),
22
+ desc: "array of CFR references for this topic"
23
+
24
+ attribute :see_also,
25
+ type: Array(:string),
26
+ desc: "array of related topic names to see also"
27
+
28
+ attribute :see,
29
+ type: Array(:string),
30
+ desc: "array of topic names to redirect to"
31
+
32
+ attribute :slug,
33
+ desc: "normalized topic identifier for URLs"
34
+
35
+ TOPICS_PATH = "v1/topics" # /current | /2024
36
+
37
+ metadata_key :meta
38
+
39
+ metadata :agencies,
40
+ type: Array(Ecfr::VersionerService::Agency),
41
+ desc: "array of agencies referenced in topics"
42
+
43
+ metadata :references,
44
+ type: Array(Ecfr::VersionerService::Reference),
45
+ desc: "array of CFR references with descriptions"
46
+
47
+ #
48
+ # Retrieves the list of all Topics
49
+ #
50
+ # @param [<Hash>] options
51
+ # @option options [String] :year ("current") the year or 'current' to retrieve topics for
52
+ # @option options [Boolean] :include_agencies (false) whether to include agencies in the response
53
+ #
54
+ # @return [<Topic>] array of Topics with their references and relationships
55
+ #
56
+ def self.all(options = {})
57
+ year = options.fetch(:year, "current")
58
+ include_agencies = options.fetch(:include_agencies, false)
59
+
60
+ params = {}
61
+ params[:include_agencies] = true if include_agencies
62
+
63
+ perform(
64
+ :get,
65
+ "#{TOPICS_PATH}/#{year}",
66
+ params: params,
67
+ perform_options: {
68
+ attributes_key: nil
69
+ }
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
@@ -19,7 +19,6 @@ module Ecfr
19
19
  # @param [<Date, String, 'current'>] date ISO string or 'current'
20
20
  # @param [<Integer, String>] title_number the title of interest
21
21
  # @param [<Hash>] options a hash of hierarchy levels for the content desired - see attributes defined in {Ecfr::Common::Hierarchy} for acceptable keys
22
- # @option options [<String>] build_id internal use only - a specific build id
23
22
  #
24
23
  # @return [<XML>] XML for the full title or pared down to the requested hierarchy
25
24
  #
@@ -40,7 +39,6 @@ module Ecfr
40
39
  # @param [<Date, String, 'current'>] date ISO string or 'current'
41
40
  # @param [<Integer, String>] title_number the title of interest
42
41
  # @param [<Hash>] options a hash of hierarchy levels for the content desired - see attributes defined in {Ecfr:Common::Hierarchy} for acceptable keys
43
- # @option options [<String>] build_id internal use only - a specific build id
44
42
  #
45
43
  # @return [<String>] URL to retreive XML content for the given parameters
46
44
  #
data/lib/ecfr.rb CHANGED
@@ -5,6 +5,7 @@ require "active_model/type"
5
5
 
6
6
  require "active_support"
7
7
  require "active_support/core_ext/class/attribute"
8
+ require "active_support/core_ext/enumerable"
8
9
  require "active_support/core_ext/hash"
9
10
  require "active_support/core_ext/module/delegation"
10
11
  require "active_support/core_ext/string"
@@ -92,3 +93,5 @@ require_relative "ecfr/search_service/base"
92
93
  require_relative "ecfr/subscriptions_service/base"
93
94
  require_relative "ecfr/varnish_cache_service/base"
94
95
  require_relative "ecfr/versioner_service/base"
96
+
97
+ require_relative "ecfr/client_configuration"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ecfr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peregrinator
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-27 00:00:00.000000000 Z
10
+ date: 2026-06-29 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activemodel
@@ -274,7 +274,6 @@ files:
274
274
  - lib/ecfr/admin_service/agency.rb
275
275
  - lib/ecfr/admin_service/api_documentation.rb
276
276
  - lib/ecfr/admin_service/base.rb
277
- - lib/ecfr/admin_service/build.rb
278
277
  - lib/ecfr/admin_service/ecfr_correction.rb
279
278
  - lib/ecfr/admin_service/ecfr_correction/cfr_reference.rb
280
279
  - lib/ecfr/admin_service/editorial_note.rb
@@ -290,6 +289,7 @@ files:
290
289
  - lib/ecfr/attribute_method_definition.rb
291
290
  - lib/ecfr/base.rb
292
291
  - lib/ecfr/client.rb
292
+ - lib/ecfr/client_configuration.rb
293
293
  - lib/ecfr/common/hierarchy.rb
294
294
  - lib/ecfr/configuration.rb
295
295
  - lib/ecfr/constants.rb
@@ -325,6 +325,7 @@ files:
325
325
  - lib/ecfr/search_service/date_facet.rb
326
326
  - lib/ecfr/search_service/facet_base.rb
327
327
  - lib/ecfr/search_service/status.rb
328
+ - lib/ecfr/search_service/timeline.rb
328
329
  - lib/ecfr/search_service/title_facet.rb
329
330
  - lib/ecfr/subscriptions_service/base.rb
330
331
  - lib/ecfr/subscriptions_service/status.rb
@@ -335,8 +336,11 @@ files:
335
336
  - lib/ecfr/testing/extensions/renderer_service/origin_extensions.rb
336
337
  - lib/ecfr/testing/extensions/search_service/content_version_result_extensions.rb
337
338
  - lib/ecfr/testing/extensions/search_service/date_facet_extensions.rb
339
+ - lib/ecfr/testing/extensions/search_service/timeline_extensions.rb
338
340
  - lib/ecfr/testing/extensions/versioner_service/ancestors_extensions.rb
341
+ - lib/ecfr/testing/extensions/versioner_service/reference_extensions.rb
339
342
  - lib/ecfr/testing/extensions/versioner_service/title_extenstions.rb
343
+ - lib/ecfr/testing/extensions/versioner_service/topic_extensions.rb
340
344
  - lib/ecfr/testing/factories/admin_service/agency_factory.rb
341
345
  - lib/ecfr/testing/factories/admin_service/cfr_reference_factory.rb
342
346
  - lib/ecfr/testing/factories/admin_service/ecfr_correction_factory.rb
@@ -347,29 +351,36 @@ files:
347
351
  - lib/ecfr/testing/factories/search_service/content_version_count_factory.rb
348
352
  - lib/ecfr/testing/factories/search_service/content_version_result_factory.rb
349
353
  - lib/ecfr/testing/factories/search_service/date_facet_factory.rb
354
+ - lib/ecfr/testing/factories/search_service/timeline_factory.rb
350
355
  - lib/ecfr/testing/factories/versioner_service/ancestors_factory.rb
351
356
  - lib/ecfr/testing/factories/versioner_service/metadata_node_info_factory.rb
352
357
  - lib/ecfr/testing/factories/versioner_service/node_summary_factory.rb
358
+ - lib/ecfr/testing/factories/versioner_service/reference_factory.rb
353
359
  - lib/ecfr/testing/factories/versioner_service/structure_factory.rb
354
360
  - lib/ecfr/testing/factories/versioner_service/title_factory.rb
361
+ - lib/ecfr/testing/factories/versioner_service/topic_factory.rb
355
362
  - lib/ecfr/testing/factory_bot_helpers/content_version.rb
356
363
  - lib/ecfr/testing/factory_bot_helpers/ecfr_gem_initialize_helpers.rb
357
364
  - lib/ecfr/testing/helpers/response_helper.rb
358
365
  - lib/ecfr/testing/strategies/ecfr_attribute_hash_strategy.rb
359
366
  - lib/ecfr/varnish_cache_service/base.rb
360
367
  - lib/ecfr/version.rb
368
+ - lib/ecfr/versioner_service/agency.rb
361
369
  - lib/ecfr/versioner_service/ancestors.rb
362
370
  - lib/ecfr/versioner_service/ancestors/metadata_node_info.rb
363
371
  - lib/ecfr/versioner_service/ancestors/node_summary.rb
364
372
  - lib/ecfr/versioner_service/api_documentation.rb
373
+ - lib/ecfr/versioner_service/authority.rb
365
374
  - lib/ecfr/versioner_service/base.rb
366
375
  - lib/ecfr/versioner_service/issue_package.rb
367
376
  - lib/ecfr/versioner_service/issue_package/issue_volume.rb
368
377
  - lib/ecfr/versioner_service/issue_package/sha_comparison.rb
369
378
  - lib/ecfr/versioner_service/issue_package/title_version.rb
379
+ - lib/ecfr/versioner_service/reference.rb
370
380
  - lib/ecfr/versioner_service/status.rb
371
381
  - lib/ecfr/versioner_service/structure.rb
372
382
  - lib/ecfr/versioner_service/title.rb
383
+ - lib/ecfr/versioner_service/topic.rb
373
384
  - lib/ecfr/versioner_service/xml_content.rb
374
385
  - lib/yard/attribute_handler.rb
375
386
  - lib/yard/metadata_handler.rb
@@ -1,38 +0,0 @@
1
- module Ecfr
2
- module AdminService
3
- class Build < Base
4
- result_key :builds
5
-
6
- attribute :id,
7
- desc: "build id"
8
- attribute :status,
9
- desc: "build status - either *Success* or *Failure*"
10
-
11
- attribute :expired,
12
- type: :boolean
13
- attribute :previewable,
14
- type: :boolean
15
-
16
- BUILDS_PATH = "v1/builds"
17
-
18
- #
19
- # Retrieve a Build by id
20
- #
21
- # @param [<String>] build_id - id of the desired build
22
- #
23
- # @return [<Build>] data for a single build
24
- #
25
- def self.find(build_id)
26
- perform(
27
- :get,
28
- build_path(build_id)
29
- )
30
- end
31
-
32
- def self.build_path(build_id)
33
- "#{BUILDS_PATH}/#{build_id}"
34
- end
35
- private_class_method :build_path
36
- end
37
- end
38
- end