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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +11 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.rspec_parallel +4 -0
  6. data/.rubocop.yml +28 -0
  7. data/.yardopts +3 -0
  8. data/CHANGELOG.md +6 -0
  9. data/Dockerfile +6 -0
  10. data/Gemfile +6 -0
  11. data/Gemfile.lock +138 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +133 -0
  14. data/Rakefile +12 -0
  15. data/bin/console +15 -0
  16. data/bin/setup +8 -0
  17. data/ecfr.gemspec +74 -0
  18. data/lib/ecfr/admin_service/agency/hierarchy.rb +27 -0
  19. data/lib/ecfr/admin_service/agency.rb +34 -0
  20. data/lib/ecfr/admin_service/api_documentation.rb +7 -0
  21. data/lib/ecfr/admin_service/base.rb +26 -0
  22. data/lib/ecfr/admin_service/build.rb +38 -0
  23. data/lib/ecfr/admin_service/ecfr_correction/cfr_reference.rb +17 -0
  24. data/lib/ecfr/admin_service/ecfr_correction.rb +78 -0
  25. data/lib/ecfr/admin_service/editorial_note/hierarchy.rb +19 -0
  26. data/lib/ecfr/admin_service/editorial_note.rb +40 -0
  27. data/lib/ecfr/admin_service/ibr_cfr_range/address.rb +17 -0
  28. data/lib/ecfr/admin_service/ibr_cfr_range/organization.rb +28 -0
  29. data/lib/ecfr/admin_service/ibr_cfr_range.rb +67 -0
  30. data/lib/ecfr/admin_service/issue/change.rb +19 -0
  31. data/lib/ecfr/admin_service/issue.rb +86 -0
  32. data/lib/ecfr/admin_service/site_notification.rb +34 -0
  33. data/lib/ecfr/admin_service/status.rb +7 -0
  34. data/lib/ecfr/attribute_caster.rb +72 -0
  35. data/lib/ecfr/attribute_method_definition.rb +92 -0
  36. data/lib/ecfr/base.rb +71 -0
  37. data/lib/ecfr/client.rb +318 -0
  38. data/lib/ecfr/common/hierarchy.rb +35 -0
  39. data/lib/ecfr/configuration.rb +58 -0
  40. data/lib/ecfr/constants.rb +21 -0
  41. data/lib/ecfr/default_documentation_setup.rb +39 -0
  42. data/lib/ecfr/default_status_setup.rb +46 -0
  43. data/lib/ecfr/diff_service/base.rb +17 -0
  44. data/lib/ecfr/diff_service/status.rb +34 -0
  45. data/lib/ecfr/extensible.rb +45 -0
  46. data/lib/ecfr/facet_attribute_method_definition.rb +47 -0
  47. data/lib/ecfr/faraday/user_agent/middleware.rb +14 -0
  48. data/lib/ecfr/ofr_profile_service/base.rb +20 -0
  49. data/lib/ecfr/ofr_profile_service/status.rb +7 -0
  50. data/lib/ecfr/parallel_client.rb +33 -0
  51. data/lib/ecfr/prince_xml_service/base.rb +17 -0
  52. data/lib/ecfr/prince_xml_service/pdf.rb +31 -0
  53. data/lib/ecfr/renderer_service/base.rb +31 -0
  54. data/lib/ecfr/renderer_service/content.rb +34 -0
  55. data/lib/ecfr/renderer_service/diff.rb +31 -0
  56. data/lib/ecfr/renderer_service/origin.rb +56 -0
  57. data/lib/ecfr/renderer_service/status.rb +7 -0
  58. data/lib/ecfr/request_representation.rb +12 -0
  59. data/lib/ecfr/search_service/api_documentation.rb +7 -0
  60. data/lib/ecfr/search_service/base.rb +23 -0
  61. data/lib/ecfr/search_service/content_version/count.rb +33 -0
  62. data/lib/ecfr/search_service/content_version/hierarchical_count.rb +17 -0
  63. data/lib/ecfr/search_service/content_version/hierarchical_count_node.rb +30 -0
  64. data/lib/ecfr/search_service/content_version/hierarchichal_result.rb +42 -0
  65. data/lib/ecfr/search_service/content_version/result.rb +110 -0
  66. data/lib/ecfr/search_service/content_version/suggestion.rb +76 -0
  67. data/lib/ecfr/search_service/content_version/summary.rb +27 -0
  68. data/lib/ecfr/search_service/content_version.rb +85 -0
  69. data/lib/ecfr/search_service/date_facet.rb +19 -0
  70. data/lib/ecfr/search_service/facet_base.rb +55 -0
  71. data/lib/ecfr/search_service/status.rb +7 -0
  72. data/lib/ecfr/search_service/title_facet.rb +18 -0
  73. data/lib/ecfr/subscriptions_service/base.rb +19 -0
  74. data/lib/ecfr/subscriptions_service/status.rb +7 -0
  75. data/lib/ecfr/subscriptions_service/subscription.rb +97 -0
  76. data/lib/ecfr/testing/extensions/admin_service/ecfr_correction_extensions.rb +13 -0
  77. data/lib/ecfr/testing/extensions/admin_service/issue_extensions.rb +13 -0
  78. data/lib/ecfr/testing/extensions/renderer_service/origin_extensions.rb +13 -0
  79. data/lib/ecfr/testing/extensions/search_service/content_version_result_extensions.rb +16 -0
  80. data/lib/ecfr/testing/extensions/search_service/date_facet_extensions.rb +13 -0
  81. data/lib/ecfr/testing/extensions/versioner_service/ancestors_extensions.rb +20 -0
  82. data/lib/ecfr/testing/extensions/versioner_service/title_extenstions.rb +16 -0
  83. data/lib/ecfr/testing/factories/admin_service/cfr_reference_factory.rb +14 -0
  84. data/lib/ecfr/testing/factories/admin_service/ecfr_correction_factory.rb +31 -0
  85. data/lib/ecfr/testing/factories/admin_service/issue_change_factory.rb +12 -0
  86. data/lib/ecfr/testing/factories/admin_service/issue_factory.rb +21 -0
  87. data/lib/ecfr/testing/factories/common/hierarchy_factory.rb +36 -0
  88. data/lib/ecfr/testing/factories/renderer_service/origin_factory.rb +32 -0
  89. data/lib/ecfr/testing/factories/search_service/content_version_count_factory.rb +20 -0
  90. data/lib/ecfr/testing/factories/search_service/content_version_result_factory.rb +76 -0
  91. data/lib/ecfr/testing/factories/search_service/date_facet_factory.rb +12 -0
  92. data/lib/ecfr/testing/factories/versioner_service/ancestors_factory.rb +26 -0
  93. data/lib/ecfr/testing/factories/versioner_service/metadata_node_info_factory.rb +15 -0
  94. data/lib/ecfr/testing/factories/versioner_service/node_summary_factory.rb +16 -0
  95. data/lib/ecfr/testing/factories/versioner_service/structure_factory.rb +57 -0
  96. data/lib/ecfr/testing/factories/versioner_service/title_factory.rb +36 -0
  97. data/lib/ecfr/testing/factory_bot_helpers/content_version.rb +38 -0
  98. data/lib/ecfr/testing/factory_bot_helpers/ecfr_gem_initialize_helpers.rb +51 -0
  99. data/lib/ecfr/testing/helpers/response_helper.rb +5 -0
  100. data/lib/ecfr/testing/strategies/ecfr_attribute_hash_strategy.rb +37 -0
  101. data/lib/ecfr/testing.rb +28 -0
  102. data/lib/ecfr/version.rb +5 -0
  103. data/lib/ecfr/versioner_service/ancestors/metadata_node_info.rb +22 -0
  104. data/lib/ecfr/versioner_service/ancestors/node_summary.rb +54 -0
  105. data/lib/ecfr/versioner_service/ancestors.rb +152 -0
  106. data/lib/ecfr/versioner_service/api_documentation.rb +7 -0
  107. data/lib/ecfr/versioner_service/base.rb +24 -0
  108. data/lib/ecfr/versioner_service/status.rb +7 -0
  109. data/lib/ecfr/versioner_service/structure.rb +120 -0
  110. data/lib/ecfr/versioner_service/title.rb +78 -0
  111. data/lib/ecfr/versioner_service/xml_content.rb +59 -0
  112. data/lib/ecfr.rb +90 -0
  113. data/lib/yard/attribute_handler.rb +87 -0
  114. data/lib/yard/metadata_handler.rb +87 -0
  115. metadata +389 -0
@@ -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