ecfr 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/.circleci/config.yml +11 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rspec_parallel +4 -0
- data/.rubocop.yml +28 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +6 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/ecfr.gemspec +74 -0
- data/lib/ecfr/admin_service/agency/hierarchy.rb +27 -0
- data/lib/ecfr/admin_service/agency.rb +34 -0
- data/lib/ecfr/admin_service/api_documentation.rb +7 -0
- data/lib/ecfr/admin_service/base.rb +26 -0
- data/lib/ecfr/admin_service/build.rb +38 -0
- data/lib/ecfr/admin_service/ecfr_correction/cfr_reference.rb +17 -0
- data/lib/ecfr/admin_service/ecfr_correction.rb +78 -0
- data/lib/ecfr/admin_service/editorial_note/hierarchy.rb +19 -0
- data/lib/ecfr/admin_service/editorial_note.rb +40 -0
- data/lib/ecfr/admin_service/ibr_cfr_range/address.rb +17 -0
- data/lib/ecfr/admin_service/ibr_cfr_range/organization.rb +28 -0
- data/lib/ecfr/admin_service/ibr_cfr_range.rb +67 -0
- data/lib/ecfr/admin_service/issue/change.rb +19 -0
- data/lib/ecfr/admin_service/issue.rb +86 -0
- data/lib/ecfr/admin_service/site_notification.rb +34 -0
- data/lib/ecfr/admin_service/status.rb +7 -0
- data/lib/ecfr/attribute_caster.rb +72 -0
- data/lib/ecfr/attribute_method_definition.rb +92 -0
- data/lib/ecfr/base.rb +71 -0
- data/lib/ecfr/client.rb +318 -0
- data/lib/ecfr/common/hierarchy.rb +35 -0
- data/lib/ecfr/configuration.rb +58 -0
- data/lib/ecfr/constants.rb +21 -0
- data/lib/ecfr/default_documentation_setup.rb +39 -0
- data/lib/ecfr/default_status_setup.rb +46 -0
- data/lib/ecfr/diff_service/base.rb +17 -0
- data/lib/ecfr/diff_service/status.rb +34 -0
- data/lib/ecfr/extensible.rb +45 -0
- data/lib/ecfr/facet_attribute_method_definition.rb +47 -0
- data/lib/ecfr/faraday/user_agent/middleware.rb +14 -0
- data/lib/ecfr/ofr_profile_service/base.rb +20 -0
- data/lib/ecfr/ofr_profile_service/status.rb +7 -0
- data/lib/ecfr/parallel_client.rb +33 -0
- data/lib/ecfr/prince_xml_service/base.rb +17 -0
- data/lib/ecfr/prince_xml_service/pdf.rb +31 -0
- data/lib/ecfr/renderer_service/base.rb +31 -0
- data/lib/ecfr/renderer_service/content.rb +34 -0
- data/lib/ecfr/renderer_service/diff.rb +31 -0
- data/lib/ecfr/renderer_service/origin.rb +56 -0
- data/lib/ecfr/renderer_service/status.rb +7 -0
- data/lib/ecfr/request_representation.rb +12 -0
- data/lib/ecfr/search_service/api_documentation.rb +7 -0
- data/lib/ecfr/search_service/base.rb +23 -0
- data/lib/ecfr/search_service/content_version/count.rb +33 -0
- data/lib/ecfr/search_service/content_version/hierarchical_count.rb +17 -0
- data/lib/ecfr/search_service/content_version/hierarchical_count_node.rb +30 -0
- data/lib/ecfr/search_service/content_version/hierarchichal_result.rb +42 -0
- data/lib/ecfr/search_service/content_version/result.rb +110 -0
- data/lib/ecfr/search_service/content_version/suggestion.rb +76 -0
- data/lib/ecfr/search_service/content_version/summary.rb +27 -0
- data/lib/ecfr/search_service/content_version.rb +85 -0
- data/lib/ecfr/search_service/date_facet.rb +19 -0
- data/lib/ecfr/search_service/facet_base.rb +55 -0
- data/lib/ecfr/search_service/status.rb +7 -0
- data/lib/ecfr/search_service/title_facet.rb +18 -0
- data/lib/ecfr/subscriptions_service/base.rb +19 -0
- data/lib/ecfr/subscriptions_service/status.rb +7 -0
- data/lib/ecfr/subscriptions_service/subscription.rb +97 -0
- data/lib/ecfr/testing/extensions/admin_service/ecfr_correction_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/admin_service/issue_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/renderer_service/origin_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/search_service/content_version_result_extensions.rb +16 -0
- data/lib/ecfr/testing/extensions/search_service/date_facet_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/versioner_service/ancestors_extensions.rb +20 -0
- data/lib/ecfr/testing/extensions/versioner_service/title_extenstions.rb +16 -0
- data/lib/ecfr/testing/factories/admin_service/cfr_reference_factory.rb +14 -0
- data/lib/ecfr/testing/factories/admin_service/ecfr_correction_factory.rb +31 -0
- data/lib/ecfr/testing/factories/admin_service/issue_change_factory.rb +12 -0
- data/lib/ecfr/testing/factories/admin_service/issue_factory.rb +21 -0
- data/lib/ecfr/testing/factories/common/hierarchy_factory.rb +36 -0
- data/lib/ecfr/testing/factories/renderer_service/origin_factory.rb +32 -0
- data/lib/ecfr/testing/factories/search_service/content_version_count_factory.rb +20 -0
- data/lib/ecfr/testing/factories/search_service/content_version_result_factory.rb +76 -0
- data/lib/ecfr/testing/factories/search_service/date_facet_factory.rb +12 -0
- data/lib/ecfr/testing/factories/versioner_service/ancestors_factory.rb +26 -0
- data/lib/ecfr/testing/factories/versioner_service/metadata_node_info_factory.rb +15 -0
- data/lib/ecfr/testing/factories/versioner_service/node_summary_factory.rb +16 -0
- data/lib/ecfr/testing/factories/versioner_service/structure_factory.rb +57 -0
- data/lib/ecfr/testing/factories/versioner_service/title_factory.rb +36 -0
- data/lib/ecfr/testing/factory_bot_helpers/content_version.rb +38 -0
- data/lib/ecfr/testing/factory_bot_helpers/ecfr_gem_initialize_helpers.rb +51 -0
- data/lib/ecfr/testing/helpers/response_helper.rb +5 -0
- data/lib/ecfr/testing/strategies/ecfr_attribute_hash_strategy.rb +37 -0
- data/lib/ecfr/testing.rb +28 -0
- data/lib/ecfr/version.rb +5 -0
- data/lib/ecfr/versioner_service/ancestors/metadata_node_info.rb +22 -0
- data/lib/ecfr/versioner_service/ancestors/node_summary.rb +54 -0
- data/lib/ecfr/versioner_service/ancestors.rb +152 -0
- data/lib/ecfr/versioner_service/api_documentation.rb +7 -0
- data/lib/ecfr/versioner_service/base.rb +24 -0
- data/lib/ecfr/versioner_service/status.rb +7 -0
- data/lib/ecfr/versioner_service/structure.rb +120 -0
- data/lib/ecfr/versioner_service/title.rb +78 -0
- data/lib/ecfr/versioner_service/xml_content.rb +59 -0
- data/lib/ecfr.rb +90 -0
- data/lib/yard/attribute_handler.rb +87 -0
- data/lib/yard/metadata_handler.rb +87 -0
- metadata +389 -0
data/lib/ecfr/client.rb
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
#
|
|
3
|
+
# Provides a configurable Faraday connection
|
|
4
|
+
# (via {Ecfr::Configuration} options) and returns an
|
|
5
|
+
# instance of the calling class.
|
|
6
|
+
#
|
|
7
|
+
# The request can be additionally modified before flight
|
|
8
|
+
# using the {Ecfr.config.request_hook} - this is useful
|
|
9
|
+
# for adding additional headers like request tracking IDs, etc.
|
|
10
|
+
#
|
|
11
|
+
class Client
|
|
12
|
+
include Ecfr::ParallelClient
|
|
13
|
+
|
|
14
|
+
class Error < StandardError
|
|
15
|
+
attr_reader :record
|
|
16
|
+
|
|
17
|
+
def initialize(record = nil, message = "")
|
|
18
|
+
@record = record
|
|
19
|
+
super(message)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class BadRequest < Error; end
|
|
24
|
+
class Forbidden < Error; end
|
|
25
|
+
class InvalidRequest < Error; end
|
|
26
|
+
class RecordNotFound < Error; end
|
|
27
|
+
class ResponseError < Error; end
|
|
28
|
+
class ServerError < Error; end
|
|
29
|
+
class Unauthorized < Error; end
|
|
30
|
+
class UnknownStatusCode < Error; end
|
|
31
|
+
|
|
32
|
+
#
|
|
33
|
+
# Faraday client configured for eCFR usage.
|
|
34
|
+
#
|
|
35
|
+
# @param [<Hash>] client_options
|
|
36
|
+
# @option client_options [<Hash>] :basic_auth :username
|
|
37
|
+
# and :password basic auth credentials
|
|
38
|
+
# @option client_options [<Hash>] :timeout value in
|
|
39
|
+
# seconds for a custom timeout value
|
|
40
|
+
# @param [<Proc>] &block the faraday connection is yielded
|
|
41
|
+
# to the provided block allowing additional customization
|
|
42
|
+
#
|
|
43
|
+
# @return [<Faraday::Connection>] Faraday connection object
|
|
44
|
+
#
|
|
45
|
+
# @note: base_url is defined in each service, but we allow an override
|
|
46
|
+
# for use cases that need to use the raw client/parallel_client
|
|
47
|
+
#
|
|
48
|
+
def self.client(base_url: nil, client_options: {}, &block)
|
|
49
|
+
client_pool(base_url || self.base_url, client_options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# Returns a client connection, either new or from cache, based
|
|
54
|
+
# on origin and client_options. This allows connection sharing
|
|
55
|
+
# across all requests to the same origin with the current thread.
|
|
56
|
+
#
|
|
57
|
+
# @param [<String>] base_url a well formed url
|
|
58
|
+
# @param [<Hash>] client_options see Ecfr::Client.client
|
|
59
|
+
#
|
|
60
|
+
# @return [<Faraday::Connection>] Faraday connection object
|
|
61
|
+
#
|
|
62
|
+
def self.client_pool(base_url, client_options)
|
|
63
|
+
uri = URI.parse(base_url)
|
|
64
|
+
|
|
65
|
+
cache_key = "ecfr-client-pool-#{uri.host}-#{Digest::MD5.hexdigest(client_options.to_s)}"
|
|
66
|
+
|
|
67
|
+
RequestStore.fetch(cache_key) do
|
|
68
|
+
Faraday.new(url: uri.to_s) do |faraday|
|
|
69
|
+
if client_options[:adapter]
|
|
70
|
+
faraday.adapter client_options[:adapter]
|
|
71
|
+
else
|
|
72
|
+
faraday.adapter :net_http_persistent, pool_size: 5
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
faraday.request :url_encoded # form-encode POST params
|
|
76
|
+
faraday.options[:timeout] = client_options[:timeout] || Ecfr.config.timeout
|
|
77
|
+
|
|
78
|
+
faraday.use Faraday::UserAgent::Middleware, Ecfr.config.user_agent
|
|
79
|
+
|
|
80
|
+
if Ecfr.config.log_http_requests
|
|
81
|
+
# https://lostisland.github.io/faraday/middleware/instrumentation
|
|
82
|
+
faraday.request :instrumentation
|
|
83
|
+
|
|
84
|
+
faraday.response :logger, Ecfr.config.logger, Ecfr.config.logger_options
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
if client_options[:basic_auth]
|
|
88
|
+
faraday.request(:basic_auth,
|
|
89
|
+
client_options.dig(:basic_auth, :username),
|
|
90
|
+
client_options.dig(:basic_auth, :password))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
yield(faraday) if block_given?
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#
|
|
99
|
+
# A wrapper around Faraday connection methods that provides
|
|
100
|
+
# support for more complex handling of the response to support
|
|
101
|
+
# our classes. This is the recommended method for most endpoints
|
|
102
|
+
# that return JSON.
|
|
103
|
+
#
|
|
104
|
+
# @param [<Symbol, String>] method lowercase HTTP verb -
|
|
105
|
+
# :get, :post, :delete, and :purge are supported
|
|
106
|
+
# @param [<String>] path added to the service base url to form
|
|
107
|
+
# the full URL for the API endpoint
|
|
108
|
+
# @param [<Hash>] params parameters to be added to the request
|
|
109
|
+
# @param [<Hash>] client_options see {Ecfr::Client.client} for
|
|
110
|
+
# valid options
|
|
111
|
+
# @param [<Hash>] perform_options
|
|
112
|
+
# @option perform_options [<Boolean>] :parse_response (true) whether
|
|
113
|
+
# return a parsed response (via JSON.parse) or just return the
|
|
114
|
+
# response body.
|
|
115
|
+
# @option perform_options [<Symbol, String>] :attributes_key the
|
|
116
|
+
# key from the JSON response to return as the results. Provides
|
|
117
|
+
# support for API endpoints that may not use a standard key for
|
|
118
|
+
# for every request path.
|
|
119
|
+
#
|
|
120
|
+
# @return [<Object>] instantiated Ecfr class with the
|
|
121
|
+
# response_status set.
|
|
122
|
+
#
|
|
123
|
+
def self.perform(method, path, params: {}, client_options: {}, perform_options: {})
|
|
124
|
+
path = [self::SERVICE_PATH, path].compact.join("/")
|
|
125
|
+
|
|
126
|
+
default_perform_options = {parse_response: true}
|
|
127
|
+
perform_options = default_perform_options.merge(perform_options)
|
|
128
|
+
|
|
129
|
+
# cache response data, not the instantiation of the class - this allows
|
|
130
|
+
# for multiple user classes to inherit from a single gem defined class
|
|
131
|
+
if Ecfr.config.cache_responses
|
|
132
|
+
cache_key = cache_key(method, path, params)
|
|
133
|
+
|
|
134
|
+
response = instrument("Ecfr::Perform #{cache_key.to_s.tr("\"", "'")}") do
|
|
135
|
+
RequestStore.fetch(cache_key) do
|
|
136
|
+
puts "Request not in eCFR gem cache, fetching..."
|
|
137
|
+
|
|
138
|
+
response = fetch(method, path, params: params, client_options: client_options)
|
|
139
|
+
|
|
140
|
+
cache_base_response(response, path, params) if respond_to?(:cache_base_response)
|
|
141
|
+
|
|
142
|
+
response
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
else
|
|
146
|
+
response = instrument("Ecfr::Perform #{path}/#{params}") do
|
|
147
|
+
fetch(method, path, params: params, client_options: client_options)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
build(
|
|
152
|
+
response: response,
|
|
153
|
+
request_data: {
|
|
154
|
+
method: method,
|
|
155
|
+
path: path,
|
|
156
|
+
params: params
|
|
157
|
+
},
|
|
158
|
+
build_options: perform_options
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.instrument(step, &block)
|
|
163
|
+
if defined?(Rack::MiniProfiler)
|
|
164
|
+
Rack::MiniProfiler.step(step) do
|
|
165
|
+
yield(block)
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
yield(block)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
private_class_method :instrument
|
|
172
|
+
|
|
173
|
+
def self.cache_key(method, path, params)
|
|
174
|
+
"#{method}:#{path}:#{Digest::MD5.hexdigest(params.sort.to_s)}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def self.fetch(method, path, params:, client_options:)
|
|
178
|
+
instrument("Ecfr::Fetch") do
|
|
179
|
+
send(method, path, params, client_options)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
private_class_method :fetch
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# Transforms the response into our objects. Primarily exists
|
|
186
|
+
# to support testing in the upstream user applications.
|
|
187
|
+
#
|
|
188
|
+
# See the .perform/.fetch method for argument signatures
|
|
189
|
+
#
|
|
190
|
+
def self.build(response:, request_data: {}, build_options: {})
|
|
191
|
+
default_build_options = {parse_response: true}
|
|
192
|
+
build_options = default_build_options.merge(build_options)
|
|
193
|
+
|
|
194
|
+
if build_options[:parse_response]
|
|
195
|
+
results = JSON.parse(response.body)
|
|
196
|
+
|
|
197
|
+
results = results[build_options[:attributes_key].to_s] if build_options[:attributes_key]
|
|
198
|
+
else
|
|
199
|
+
results = response.body
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
options = {
|
|
203
|
+
response_status: response.status,
|
|
204
|
+
request_data: request_data
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
options.merge!(build_options[:init_data]) if build_options[:init_data]
|
|
208
|
+
|
|
209
|
+
# json results are dynamically cast into subclasses -
|
|
210
|
+
# in order support upstream inheritance based overrides
|
|
211
|
+
# of these subclasses, we capture when inheritance happens
|
|
212
|
+
# and instantiate that class instead
|
|
213
|
+
if const_defined?("#{self}::KLASS")
|
|
214
|
+
self::KLASS.new(results, options)
|
|
215
|
+
else
|
|
216
|
+
new(results, options)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def self.get(path, params = {}, client_options = {})
|
|
221
|
+
c = client(client_options: client_options)
|
|
222
|
+
|
|
223
|
+
execute do
|
|
224
|
+
c.get(path, params) do |req|
|
|
225
|
+
Ecfr.config.request_hook.call(req)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.post(path, params = {}, client_options = {})
|
|
231
|
+
c = client(client_options: client_options)
|
|
232
|
+
|
|
233
|
+
execute do
|
|
234
|
+
c.post(path, params) do |req|
|
|
235
|
+
Ecfr.config.request_hook.call(req)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def self.delete(path, params = {}, client_options = {})
|
|
241
|
+
c = client(client_options: client_options)
|
|
242
|
+
|
|
243
|
+
execute do
|
|
244
|
+
c.delete(path, params) do |req|
|
|
245
|
+
Ecfr.config.request_hook.call(req)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
Faraday::Connection::METHODS << :purge
|
|
251
|
+
def self.purge(path, params = {}, client_options = {})
|
|
252
|
+
c = client(client_options: client_options)
|
|
253
|
+
|
|
254
|
+
execute do
|
|
255
|
+
c.run_request(:purge, path, nil, nil) do |request|
|
|
256
|
+
request.params.update(params)
|
|
257
|
+
Ecfr.config.request_hook.call(request)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
#
|
|
263
|
+
# Wrapper around a Faraday response to handle known response
|
|
264
|
+
# statuses and wrap them in Ecfr specific errors that can
|
|
265
|
+
# be handled upstream in a consistent manner.
|
|
266
|
+
#
|
|
267
|
+
# @param [<Proc>] &block Faraday response
|
|
268
|
+
#
|
|
269
|
+
# @return [<Faraday::Response, Client::Error>]
|
|
270
|
+
#
|
|
271
|
+
def self.execute(&block)
|
|
272
|
+
handle_response(
|
|
273
|
+
yield(block)
|
|
274
|
+
)
|
|
275
|
+
rescue Faraday::ConnectionFailed
|
|
276
|
+
raise ResponseError.new(nil, "Hostname lookup failed")
|
|
277
|
+
rescue Faraday::TimeoutError
|
|
278
|
+
raise ResponseError.new(nil, "Request timed out")
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
#
|
|
282
|
+
# Wrapper to return 4XX-5XX status codes as error objects.
|
|
283
|
+
# We attempt to parse the response body to check for error
|
|
284
|
+
# messages in JSON form and pass those along.
|
|
285
|
+
#
|
|
286
|
+
# @param [<Faraday::Response>] response
|
|
287
|
+
#
|
|
288
|
+
# @return [<Error>]
|
|
289
|
+
#
|
|
290
|
+
def self.handle_response(response)
|
|
291
|
+
return response if [200, 201, 204].include?(response.status)
|
|
292
|
+
|
|
293
|
+
# handle errors
|
|
294
|
+
begin
|
|
295
|
+
errors = JSON.parse(response.body)
|
|
296
|
+
rescue
|
|
297
|
+
errors = nil
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
case response.status
|
|
301
|
+
when 400
|
|
302
|
+
raise BadRequest.new(errors)
|
|
303
|
+
when 401
|
|
304
|
+
raise Unauthorized.new(errors)
|
|
305
|
+
when 403
|
|
306
|
+
raise Forbidden.new(errors)
|
|
307
|
+
when 404
|
|
308
|
+
raise RecordNotFound
|
|
309
|
+
when 422
|
|
310
|
+
raise InvalidRequest
|
|
311
|
+
when 500
|
|
312
|
+
raise ServerError.new(errors)
|
|
313
|
+
else
|
|
314
|
+
raise UnknownStatusCode.new(errors, "Status code: #{response.status}")
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module Common
|
|
3
|
+
class Hierarchy
|
|
4
|
+
include AttributeMethodDefinition
|
|
5
|
+
extend Extensible
|
|
6
|
+
|
|
7
|
+
attribute :title,
|
|
8
|
+
desc: "Title number"
|
|
9
|
+
attribute :subtitle,
|
|
10
|
+
desc: "Subtitle identifier"
|
|
11
|
+
attribute :chapter,
|
|
12
|
+
desc: "Chapter indentifier"
|
|
13
|
+
attribute :subchapter,
|
|
14
|
+
desc: "Subchapter identifier"
|
|
15
|
+
attribute :part,
|
|
16
|
+
desc: "Part identifier"
|
|
17
|
+
attribute :subpart,
|
|
18
|
+
desc: "Subpart identifier"
|
|
19
|
+
attribute :subject_group,
|
|
20
|
+
desc: "Subject group identifier"
|
|
21
|
+
attribute :section,
|
|
22
|
+
desc: "Section identifier"
|
|
23
|
+
attribute :appendix,
|
|
24
|
+
desc: "Appendix identifier"
|
|
25
|
+
attribute :paragraph,
|
|
26
|
+
desc: "Paragraph citation"
|
|
27
|
+
|
|
28
|
+
def to_hash
|
|
29
|
+
@attributes.each_with_object({}) { |attr, hsh|
|
|
30
|
+
hsh[attr[0]] = attr[1]
|
|
31
|
+
}.compact.symbolize_keys
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
#
|
|
3
|
+
# Provides configuration of various aspects of the gem
|
|
4
|
+
#
|
|
5
|
+
class Configuration
|
|
6
|
+
class MissingGem < StandardError; end
|
|
7
|
+
class MissingUserAgent < StandardError; end
|
|
8
|
+
|
|
9
|
+
CONFIG_DEFAULTS = {
|
|
10
|
+
base_url: "https://www.ecfr.gov",
|
|
11
|
+
ofr_profile_service_base_url: "https://www.federalregister.gov/my/profile",
|
|
12
|
+
# service url overrides
|
|
13
|
+
admin_service_url: nil,
|
|
14
|
+
diff_service_url: nil,
|
|
15
|
+
ofr_profile_service_url: nil,
|
|
16
|
+
prince_xml_service_url: nil,
|
|
17
|
+
renderer_service_url: nil,
|
|
18
|
+
search_service_url: nil,
|
|
19
|
+
subscriptions_service_url: nil,
|
|
20
|
+
versioner_service_url: nil,
|
|
21
|
+
# basic auth - some endpoints require auth
|
|
22
|
+
ecfr_basic_auth_username: nil,
|
|
23
|
+
ecfr_basic_auth_password: nil,
|
|
24
|
+
# request modification
|
|
25
|
+
request_hook: ->(req) { req },
|
|
26
|
+
user_agent: nil,
|
|
27
|
+
# client modifications
|
|
28
|
+
timeout: 10,
|
|
29
|
+
prince_xml_service_pdf_timeout: 10,
|
|
30
|
+
# response modification
|
|
31
|
+
log_http_requests: false,
|
|
32
|
+
logger: nil,
|
|
33
|
+
logger_options: {},
|
|
34
|
+
# caching
|
|
35
|
+
cache_responses: false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
attr_accessor(*CONFIG_DEFAULTS.each.map { |k, v| k })
|
|
39
|
+
|
|
40
|
+
def initialize
|
|
41
|
+
CONFIG_DEFAULTS.each do |k, v|
|
|
42
|
+
instance_variable_set("@#{k}", v)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate!
|
|
47
|
+
raise MissingUserAgent, "A user agent must be provided" if user_agent.blank?
|
|
48
|
+
|
|
49
|
+
if cache_responses
|
|
50
|
+
begin
|
|
51
|
+
Gem::Specification.find_by_name("request_store")
|
|
52
|
+
rescue Gem::LoadError
|
|
53
|
+
raise MissingGem, "the RequestStore gem must be installed if response caching is enabled"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module Constants
|
|
3
|
+
module ChangeTypes
|
|
4
|
+
KNOWN_CHANGE_TYPES = {
|
|
5
|
+
cross_references: "Regulations linked from the above-dated Federal Register:",
|
|
6
|
+
delayed: "Regulations delayed in the above-dated Federal Register:",
|
|
7
|
+
delayed_withdrawn: "Regulations delayed or withdrawn in the above-dated Federal Register:",
|
|
8
|
+
delayed_withdrawn_extended: "Regulations delayed, withdrawn, or extended in the above-dated Federal Register:",
|
|
9
|
+
effective: "Effective regulations inserted from the above-dated Federal Register:",
|
|
10
|
+
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:",
|
|
12
|
+
extended: "Regulations extended in the above-dated Federal Register",
|
|
13
|
+
initial: "Initial import of this content - change type is indeterminate"
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Hierarchy
|
|
18
|
+
HIERARCHY_LEVELS = %w[title subtitle chapter subchapter part subpart subject_group section appendix]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module DefaultDocumentationSetup
|
|
3
|
+
module ClassMethods
|
|
4
|
+
DOCUMENTATION_PATH = "documentation.json"
|
|
5
|
+
|
|
6
|
+
def api_documentation(args = {})
|
|
7
|
+
perform(
|
|
8
|
+
documentation_config[:method],
|
|
9
|
+
DOCUMENTATION_PATH,
|
|
10
|
+
params: args.compact
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# .documentation_config is provided to support use cases
|
|
15
|
+
# in which the user wants to construct their own
|
|
16
|
+
# handling of status checks, e.g. to wrap in custom
|
|
17
|
+
# errors or run in parallel
|
|
18
|
+
def documentation_config
|
|
19
|
+
{
|
|
20
|
+
url: [
|
|
21
|
+
base_url,
|
|
22
|
+
self::SERVICE_PATH,
|
|
23
|
+
"/#{DOCUMENTATION_PATH}"
|
|
24
|
+
].reject(&:blank?).join,
|
|
25
|
+
method: :get
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.included(base)
|
|
31
|
+
base.instance_eval do
|
|
32
|
+
attribute :description
|
|
33
|
+
attribute :paths
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
base.extend(ClassMethods)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module DefaultStatusSetup
|
|
3
|
+
module ClassMethods
|
|
4
|
+
STATUS_PATH = "status.json"
|
|
5
|
+
|
|
6
|
+
def status
|
|
7
|
+
perform(
|
|
8
|
+
status_config[:method],
|
|
9
|
+
STATUS_PATH
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# .status_config is provided to support use cases
|
|
14
|
+
# in which the user wants to construct their own
|
|
15
|
+
# handling of status checks, e.g. to wrap in custom
|
|
16
|
+
# errors or run in parallel
|
|
17
|
+
def status_config
|
|
18
|
+
url = [
|
|
19
|
+
base_url,
|
|
20
|
+
self::SERVICE_PATH,
|
|
21
|
+
STATUS_PATH
|
|
22
|
+
].reject(&:blank?)
|
|
23
|
+
.join("/")
|
|
24
|
+
.gsub(/(?<!:)\/\//, "/") # remove extraneous double slashes '//'
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
url: url,
|
|
28
|
+
method: :get
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.included(base)
|
|
34
|
+
base.instance_eval do
|
|
35
|
+
attribute :checks
|
|
36
|
+
attribute :status
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
base.extend(ClassMethods)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def status_code
|
|
43
|
+
response_status
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module DiffService
|
|
3
|
+
class Base < Ecfr::Base
|
|
4
|
+
require_relative "status"
|
|
5
|
+
|
|
6
|
+
SERVICE_PATH = "/api/diff"
|
|
7
|
+
|
|
8
|
+
def self.base_url
|
|
9
|
+
Ecfr.config.diff_service_url || Ecfr.config.base_url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.service_name
|
|
13
|
+
"Diff Service"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module DiffService
|
|
3
|
+
class Status < Base
|
|
4
|
+
include DefaultStatusSetup
|
|
5
|
+
|
|
6
|
+
STATUS_PATH = "v1"
|
|
7
|
+
|
|
8
|
+
def self.status
|
|
9
|
+
perform(
|
|
10
|
+
status_config[:method],
|
|
11
|
+
STATUS_PATH,
|
|
12
|
+
params: status_config[:options],
|
|
13
|
+
perform_options: {parse_response: false}
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# .status_config is provided to support use cases
|
|
18
|
+
# in which the user wants to construct their own
|
|
19
|
+
# handling of status checks, e.g. to wrap in custom
|
|
20
|
+
# errors or run in parallel
|
|
21
|
+
def self.status_config
|
|
22
|
+
{
|
|
23
|
+
url: "#{base_url}/#{SERVICE_PATH}/#{STATUS_PATH}",
|
|
24
|
+
method: :post,
|
|
25
|
+
response_type: "html",
|
|
26
|
+
options: {
|
|
27
|
+
new: "old test",
|
|
28
|
+
old: "new test"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
# Users of this gem may want to subclass our API client classes to add
|
|
3
|
+
# app-specific functionality; for example, adding to functionality of the
|
|
4
|
+
# `Ancestor` or `NodeSummary` classes.
|
|
5
|
+
#
|
|
6
|
+
# Because our the gem's client classes instantiate other objects (eg Ancestors
|
|
7
|
+
# creates NodeSummary objects), we need to make it possible for the user to
|
|
8
|
+
# get instances of their own NodeSummary subclass created when they use their
|
|
9
|
+
# Ancestor subclass
|
|
10
|
+
#
|
|
11
|
+
# To support upstream modification of these classes we record when inheritance
|
|
12
|
+
# happens and use that to instantiate the expected class at run time.
|
|
13
|
+
#
|
|
14
|
+
# This means that these subclasses can only be inherited from / modified by a
|
|
15
|
+
# single upstream class (and will always be instantiated as that class).
|
|
16
|
+
# We skip this bookkeeping when the inheritance is from one of the classes
|
|
17
|
+
# that directly inherits from a service Base class as it is reasonable
|
|
18
|
+
# to expect that inheritance in that scenario works as all other Ruby
|
|
19
|
+
# inheritance.
|
|
20
|
+
#
|
|
21
|
+
module Extensible
|
|
22
|
+
class AlreadyExtendedError < StandardError; end
|
|
23
|
+
|
|
24
|
+
def inherited(subclass)
|
|
25
|
+
# Skip for class inheritance within the gem.
|
|
26
|
+
return if subclass.to_s.starts_with?("Ecfr::")
|
|
27
|
+
|
|
28
|
+
# Skip for inheritance tracking of top level gem classes
|
|
29
|
+
return if superclass.to_s.ends_with?("Service::Base")
|
|
30
|
+
|
|
31
|
+
# Only set if not already defined (constants can't be redefined)
|
|
32
|
+
if const_defined?(:KLASS)
|
|
33
|
+
# during autoreloading in dev type environments subclassing will appear
|
|
34
|
+
# to happen twice - we can safely ignore that - if the class differs
|
|
35
|
+
# from that already defined then we want to notify the user
|
|
36
|
+
if self::KLASS.to_s != subclass.to_s
|
|
37
|
+
raise AlreadyExtendedError,
|
|
38
|
+
"#{name} has already been extended by '#{self::KLASS}'. Subclasses can not be extended by inheritance multiple times. #{subclass}"
|
|
39
|
+
end
|
|
40
|
+
else
|
|
41
|
+
const_set(:KLASS, subclass)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module FacetAttributeMethodDefinition
|
|
3
|
+
module ClassMethods
|
|
4
|
+
def attribute(*attribute)
|
|
5
|
+
as = attribute.last.delete(:as)
|
|
6
|
+
|
|
7
|
+
case as
|
|
8
|
+
when :key
|
|
9
|
+
self.fk = attribute.first.to_s
|
|
10
|
+
when :value
|
|
11
|
+
self.fv = attribute.first.to_s
|
|
12
|
+
else
|
|
13
|
+
raise "must set :as to either :key or :value, was: #{as}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
super(*attribute)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.included(base)
|
|
21
|
+
base.instance_eval do
|
|
22
|
+
class_attribute :fk
|
|
23
|
+
class_attribute :fv
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
base.extend(ClassMethods)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(results, options = {})
|
|
30
|
+
default_options = {base: true}
|
|
31
|
+
options = default_options.merge(options)
|
|
32
|
+
|
|
33
|
+
if !options[:base]
|
|
34
|
+
key, value = results
|
|
35
|
+
results = {}
|
|
36
|
+
results[self.class.fk] = key
|
|
37
|
+
results[self.class.fv] = value
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
super(results, options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def count_for(key)
|
|
44
|
+
attributes[self.class.result_root][key]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Faraday
|
|
2
|
+
module UserAgent
|
|
3
|
+
class Middleware < Faraday::Middleware
|
|
4
|
+
def initialize(app, user_agent)
|
|
5
|
+
super(app)
|
|
6
|
+
@user_agent = user_agent
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def on_request(env)
|
|
10
|
+
env[:request_headers]["User-Agent"] = @user_agent
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Ecfr
|
|
2
|
+
module OfrProfileService
|
|
3
|
+
class Base < Ecfr::Base
|
|
4
|
+
require_relative "status"
|
|
5
|
+
|
|
6
|
+
# path is relative because we expect the ofr profile
|
|
7
|
+
# api to be mounted at a subpath and an absolute url
|
|
8
|
+
# here will cause the path in the config to be lost
|
|
9
|
+
SERVICE_PATH = "api/profile"
|
|
10
|
+
|
|
11
|
+
def self.base_url
|
|
12
|
+
Ecfr.config.ofr_profile_service_url || Ecfr.config.ofr_profile_service_base_url
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.service_name
|
|
16
|
+
"OFR Profile"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|