ecfr 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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