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,12 @@
1
+ FactoryBot.define do
2
+ factory :date_facet, class: "Ecfr::SearchService::DateFacet" do
3
+ skip_create
4
+
5
+ date { "2017-01-01" }
6
+ count { 1 }
7
+
8
+ initialize_with {
9
+ new(attributes.deep_stringify_keys)
10
+ }
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ FactoryBot.define do
2
+ factory :ancestors, class: "Ecfr::VersionerService::Ancestors" do
3
+ skip_create
4
+
5
+ ancestors {
6
+ [ecfr_attribute_hash(:node_summary)]
7
+ }
8
+
9
+ metadata {
10
+ {
11
+ previous_node: ecfr_attribute_hash(:metadata_node_info),
12
+ next_node: ecfr_attribute_hash(:metadata_node_info)
13
+ }
14
+ }
15
+
16
+ trait :with_structure do
17
+ structure { ecfr_attribute_hash(:structure)["data"] }
18
+ end
19
+
20
+ source_xml_filesize { 2096 }
21
+
22
+ initialize_with {
23
+ new(attributes.deep_stringify_keys)
24
+ }
25
+ end
26
+ end
@@ -0,0 +1,15 @@
1
+ FactoryBot.define do
2
+ factory :metadata_node_info, class: "Ecfr::VersionerService::Ancestors::MetadataNodeInfo" do
3
+ skip_create
4
+
5
+ hierarchy {
6
+ ecfr_attribute_hash(:hierarchy, :with_defaults)
7
+ }
8
+
9
+ label { "Part 100 - Standards of Conduct" }
10
+
11
+ initialize_with {
12
+ new(attributes.deep_stringify_keys)
13
+ }
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ FactoryBot.define do
2
+ factory :node_summary, class: "Ecfr::VersionerService::Ancestors::MetadataNodeInfo" do
3
+ skip_create
4
+
5
+ identifier { "3" }
6
+ label { "Title 3 - The President" }
7
+ label_level { "Title 3" }
8
+ label_description { "The President" }
9
+ reserved { false }
10
+ type { "title" }
11
+
12
+ initialize_with {
13
+ new(attributes.deep_stringify_keys)
14
+ }
15
+ end
16
+ end
@@ -0,0 +1,57 @@
1
+ FactoryBot.define do
2
+ factory :structure, class: "Ecfr::VersionerService::Structure" do
3
+ skip_create
4
+
5
+ data {
6
+ {
7
+ identifier: "3",
8
+ label: "Title 3 - The President",
9
+ label_level: "Title 3",
10
+ label_description: "The President",
11
+ reserved: false,
12
+ type: "title",
13
+ children: [
14
+ {
15
+ identifier: "I",
16
+ label: " Chapter I - Executive Office of the President",
17
+ label_level: " Chapter I",
18
+ label_description: "Executive Office of the President",
19
+ reserved: false,
20
+ type: "chapter",
21
+ children: [
22
+ {
23
+ identifier: "101",
24
+ label: "Part 101 - Public Information Provisions of the Administrative Procedures Act",
25
+ label_level: "Part 101",
26
+ label_description: "Public Information Provisions of the Administrative Procedures Act",
27
+ reserved: false,
28
+ type: "part",
29
+ volumes: [
30
+ "1"
31
+ ],
32
+ children: [
33
+ {
34
+ identifier: "101.1",
35
+ label: "§ 101.1 Executive Office of the President.",
36
+ label_level: "§ 101.1",
37
+ label_description: "Executive Office of the President.",
38
+ reserved: false,
39
+ type: "section",
40
+ volumes: [
41
+ "1"
42
+ ],
43
+ received_on: "2018-10-17T01:00:00-0400"
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
49
+ ]
50
+ }.to_json
51
+ }
52
+
53
+ initialize_with {
54
+ new(data)
55
+ }
56
+ end
57
+ end
@@ -0,0 +1,36 @@
1
+ FactoryBot.define do
2
+ factory :title, class: "Ecfr::VersionerService::Title" do
3
+ skip_create
4
+
5
+ name { "General Provisions" }
6
+ number { 1 }
7
+
8
+ processing_in_progress { false }
9
+ reserved { false }
10
+
11
+ latest_amended_on { nil }
12
+ latest_issue_date { nil }
13
+ up_to_date_as_of { nil }
14
+
15
+ meta {
16
+ {
17
+ date: metadata_date,
18
+ import_in_progress: import_in_progress
19
+ }
20
+ }
21
+
22
+ transient do
23
+ metadata_date { nil }
24
+ import_in_progress { false }
25
+ end
26
+
27
+ initialize_with {
28
+ new(attributes.deep_stringify_keys)
29
+ }
30
+
31
+ trait :processing do
32
+ processing_in_progress { true }
33
+ import_in_progress { true }
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ module FactoryBotHelpers
2
+ module ContentVersion
3
+ extend self
4
+
5
+ def hierarchy_headings(attributes)
6
+ headings = attributes[:hierarchy].compact.each_with_object({}) do |attr, hsh|
7
+ hsh[attr[0]] = if attr[0] == :section
8
+ "§ #{attr[1]}"
9
+ elsif attr[0] == :appendix
10
+ attr[1]
11
+ else
12
+ [attr[0].to_s.capitalize, attr[1]].join(" ")
13
+ end
14
+ end
15
+
16
+ headings.merge(attributes[:hierarchy_headings])
17
+ end
18
+
19
+ def headings(attributes)
20
+ default_headings = {
21
+ title: "Energy",
22
+ subtitle: "Other Regulations Relating to Nuclear Power",
23
+ chapter: "Nuclear Regulatory Commission",
24
+ part: "Agency Rules of Practice and Procedure",
25
+ subpart: "Procedure for Imposing Requirements",
26
+ subject_group: "Serious Civil Penalties",
27
+ section: "Civil penalties",
28
+ appendix: "Possibilities, Procedures and Probabilities"
29
+ }
30
+
31
+ headings = attributes[:hierarchy].compact.each_with_object({}) do |attr, hsh|
32
+ hsh[attr[0]] = default_headings[attr[0]]
33
+ end
34
+
35
+ headings.merge(attributes[:headings])
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ module FactoryBotHelpers
2
+ module EcfrGemHelpers
3
+ extend self
4
+
5
+ # Allows an upstream user wants to override the factory defined in this gem
6
+ # be instantiated as another class defined in thier applicaiton (inheriting
7
+ # from a gem defined class). See the Extensible module for more details.
8
+ def override_factory_class(phactory, klass)
9
+ FactoryBot.modify do
10
+ factory phactory do
11
+ initialize_with {
12
+ attrs = FactoryBotHelpers::EcfrGemHelpers.pre_initialize_attrs_for(
13
+ phactory,
14
+ attributes
15
+ )
16
+ attrs.deep_stringify_keys! if attrs.respond_to?(:deep_stringify_keys)
17
+ klass.constantize.new(attrs)
18
+ }
19
+ end
20
+ end
21
+ end
22
+
23
+ # Provides a single method that any factory can call for attribute
24
+ # manipulation and is used to when override_factory_class is used
25
+ # to generate a factory definied in this gem but as another class
26
+ # (that inherits from a gem defined class) defined in their application.
27
+ def pre_initialize_attrs_for(factory, attrs)
28
+ case factory
29
+ when :content_version_result
30
+ pre_initialize_attrs_for_content_version_result(attrs)
31
+ when :structure
32
+ attrs[:data]
33
+ else
34
+ attrs
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ # These preinitialize helpers allow us to modify attributes before they are
41
+ # passed to the factory class for instantiation.
42
+
43
+ def pre_initialize_attrs_for_content_version_result(attrs)
44
+ # use some helpers to generate these structures based on the data provided
45
+ attrs.merge(
46
+ headings: FactoryBotHelpers::ContentVersion.headings(attrs).stringify_keys,
47
+ hierarchy_headings: FactoryBotHelpers::ContentVersion.hierarchy_headings(attrs).stringify_keys
48
+ )
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ module ResponseHelper
2
+ def stubbed_response(body, status = 200)
3
+ OpenStruct.new(body: body, status: status)
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ #
2
+ # A build strategy that take the initialized factory and returns the
3
+ # attribute hash. The attribute hash or array of attribute hashes can then
4
+ # be passed to the .build method on the Ecfr class being testing to receive
5
+ # a response object.
6
+ #
7
+ class EcfrAttributeHashStrategy
8
+ def initialize
9
+ @strategy = FactoryBot.strategy_by_name(:build).new
10
+ end
11
+
12
+ delegate :association, to: :@strategy
13
+
14
+ def result(evaluation)
15
+ hsh = {}
16
+
17
+ hsh.merge!(evaluation.object.attributes) if evaluation.object.attributes
18
+
19
+ # handle direct json results like structure that aren't cast to attributes
20
+ if evaluation.object.respond_to?(:data) && evaluation.object.data
21
+ hsh[:data] = evaluation.object.data
22
+ end
23
+
24
+ # handle classes that return metadata
25
+ key = if evaluation.object.class.respond_to?(:metadata_accessor) &&
26
+ evaluation.object.class.metadata_accessor
27
+ evaluation.object.class.metadata_accessor
28
+ else
29
+ "metadata"
30
+ end
31
+ if evaluation.object.respond_to?(:metadata) && evaluation.object.metadata
32
+ hsh[key] = evaluation.object.metadata.attributes
33
+ end
34
+
35
+ hsh.deep_stringify_keys
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # we expect the client that includes the testing items to have
4
+ # factory bot installed
5
+ begin
6
+ Gem::Specification.find_by_name("factory_bot")
7
+ require "factory_bot"
8
+ rescue Gem::LoadError
9
+ end
10
+
11
+ # custom factory bot strategies
12
+ require_relative "testing/strategies/ecfr_attribute_hash_strategy"
13
+ FactoryBot.register_strategy(:ecfr_attribute_hash, EcfrAttributeHashStrategy)
14
+
15
+ # require all our test support files
16
+ current_path = __dir__
17
+ [
18
+ # generic testing helpers
19
+ "helpers",
20
+ # factory bot specific helper methods
21
+ "factory_bot_helpers",
22
+ # testing specific methods for each class
23
+ "extensions",
24
+ # factories
25
+ "factories"
26
+ ].each do |dir|
27
+ Dir.glob(File.join(current_path, "testing/#{dir}/**/*.rb")) { |f| require f }
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ecfr
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,22 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class Ancestors
4
+ class MetadataNodeInfo
5
+ include AttributeMethodDefinition
6
+
7
+ attribute :hierarchy,
8
+ type: Ecfr::Common::Hierarchy,
9
+ desc: "the heirarchy this node resides in"
10
+
11
+ attribute :label,
12
+ desc: "the display value of the hierarchy"
13
+
14
+ # currently the API returns null for each key in the object
15
+ # so we need to do an attribute check
16
+ def present?
17
+ @attributes.compact.present?
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class Ancestors
4
+ #
5
+ # This class provides a summary of important data for the
6
+ # level in the hierarchy that it represents.
7
+ #
8
+ # Some levels of the hierarchy (generally subject groups)
9
+ # use generated identifiers because they are not designated
10
+ # potions of the CFR hierarchy and thus are not completely
11
+ # stable over time.
12
+ #
13
+ class NodeSummary
14
+ include AttributeMethodDefinition
15
+ extend Extensible
16
+
17
+ attribute :descendant_range,
18
+ desc: "the range of sections that are descendents of this level"
19
+ attribute :identifier,
20
+ desc: "the shortened identifier portion of the
21
+ CFR level"
22
+ attribute :label,
23
+ desc: "the full description of the CFR level"
24
+ attribute :label_level,
25
+ desc: "the full identifier portion of the CFR level"
26
+ attribute :label_description,
27
+ desc: "the descriptive portion of the CFR level"
28
+ attribute :type,
29
+ desc: "the type of CFR level - the value will be a
30
+ stringified version of the attribute keys in
31
+ {Ecfr::Common::Hierarchy}"
32
+
33
+ attribute :generated_id,
34
+ type: :boolean,
35
+ desc: "whether the identifier was generated"
36
+ attribute :reserved,
37
+ type: :boolean,
38
+ desc: "whether this level represents a portion of
39
+ the CFR that is Reserved"
40
+
41
+ attribute :volumes,
42
+ type: Array(:string),
43
+ desc: "the Volumes this level of the hierarchy was present
44
+ within in the print edition - only appears on Part levels and below"
45
+
46
+ # NOTE: this is autogenerated when the parent Ancestors
47
+ # class is instantiated and not part of the API response
48
+ attribute :hierarchy,
49
+ type: Ecfr::Common::Hierarchy,
50
+ desc: "the hierarchy for this node"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,152 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ #
4
+ # Ancestors is used to retrieve data about the hierarchical
5
+ # structure of the CFR. The `.find` method expects to be
6
+ # provided a limited hierarchy, for example just a title
7
+ # and part - and will return the ancestors in the hierarchy
8
+ # that part is located within.
9
+ #
10
+ # As a performance optimization the full structure hierarchy
11
+ # to the provided hierarchy can also be returned as part of
12
+ # the response.
13
+ #
14
+ # Previous and next node attributes are used for navigating
15
+ # around the same level of hierarchy, for example from
16
+ # subpart to subpart, or from section to section even when
17
+ # the next section is in a sibling part (last section of
18
+ # one subpart -> first section of the next).
19
+ #
20
+ # Source xml filesize is an estimate of the size of the
21
+ # content that would be returned if the provided hierarchy
22
+ # was retrieved.
23
+ #
24
+ class Ancestors < Base
25
+ require_relative "ancestors/metadata_node_info"
26
+ require_relative "ancestors/node_summary"
27
+
28
+ attribute :ancestors,
29
+ type: Array(NodeSummary),
30
+ desc: "an array of ancestor hierarchy levels of the hierarchy found"
31
+
32
+ # previously this was structure_as_hierarchy
33
+ attribute :structure,
34
+ type: Ecfr::VersionerService::Structure,
35
+ value_key: "structure",
36
+ options: {format: "sideloaded", self: true},
37
+ desc: "the full hierarchy to the requested (limited) hierarchy"
38
+
39
+ metadata :previous_node,
40
+ type: MetadataNodeInfo,
41
+ desc: "the previous node in the hierarchy"
42
+ metadata :next_node,
43
+ type: MetadataNodeInfo,
44
+ desc: "the next node in the hierarchy"
45
+
46
+ metadata :source_xml_filesize,
47
+ type: :integer,
48
+ desc: "the approximate file size of the xml for the hierarchy found"
49
+
50
+ ANCESTORS_PATH = "v1/ancestry"
51
+
52
+ #
53
+ # Retrive ancestor date about a given (limited) hierarchy
54
+ # on a particular date
55
+ #
56
+ # @param [<Date, String>] date Date object, ISO date string, or 'current'
57
+ # @param [<Integer>] title_number the title number to search within
58
+ # @param [<Ecfr::Common::Hierarchy>] hierarchy_args a set
59
+ # of hierarchy keys that identify the content for which
60
+ # the full hierarchy should be found
61
+ # @param [<Hash>] options various options that can change
62
+ # the behavior of the response
63
+ # @option options [<Boolean>] :structure used in combination
64
+ # with the :descendent_depth option to include the structure
65
+ # hierarchy of the requested content
66
+ # @option options [<Integer>] :descendant_depth the number of
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
+ # @option options [<Boolean>] :metadata whether to include metadata in
71
+ # the response
72
+ # @option options [<Integer>] :max_items
73
+ # @option options [<Boolean>] :partial_ancestors_ok
74
+ # @option options [<Boolean>] :smart_folding
75
+ #
76
+ # @return [<Ancestors>]
77
+ #
78
+ def self.find(date:, title_number:, hierarchy_args: {}, options: {})
79
+ # ActiveSupport::HashWithIndifferentAccess does not have a ! method
80
+ hierarchy_args = hierarchy_args.symbolize_keys
81
+ options = options.symbolize_keys
82
+
83
+ options = hierarchy_args.merge(options).except(:title, :metadata)
84
+ options[:metadata] = true
85
+
86
+ perform(
87
+ :get,
88
+ ancestors_path(date, title_number),
89
+ params: options
90
+ )
91
+ end
92
+
93
+ def self.ancestors_path(date, title_number)
94
+ "#{ANCESTORS_PATH}/#{date}/title-#{title_number}"
95
+ end
96
+ private_class_method :ancestors_path
97
+
98
+ # these parameters add items to the base response but do not affect our
99
+ # ability to cache that base response
100
+ PARAMETERS_NOT_AFFECTING_BASE_RESPONSE = %w[metadata structure descendant_depth]
101
+
102
+ # if the request only includes optional parameters that don't affect the
103
+ # basic part of the response, then we can catch a base form of that
104
+ # response by removing those optional return items (this keeps the cache
105
+ # consistent across request by not including extra items).
106
+ def self.cache_base_response(response, path, params)
107
+ if only_safe_params?(params)
108
+ cache_key = cache_key(method, path, {})
109
+ RequestStore.fetch(cache_key) do
110
+ response.except(:metadata, :structure)
111
+ end
112
+ end
113
+ end
114
+ private_class_method :cache_base_response
115
+
116
+ def self.only_safe_params?(params)
117
+ # are all of the params keys included in the list?
118
+ params.keys.all? { |p| PARAMETERS_NOT_AFFECTING_BASE_RESPONSE.include?(p) }
119
+ end
120
+
121
+ # override enumerable setup because we don't have
122
+ # a single result root key
123
+ delegate(*SUPPORTED_ARRAY_ACCESSORS, to: :ancestors)
124
+ alias_method :all, :ancestors
125
+
126
+ def each
127
+ ancestors.each { |result| yield result }
128
+ end
129
+
130
+ #
131
+ # We overload the inherited initialize method in order to modify how we
132
+ # represent the response to the end user.
133
+ # - we add a `hierarchy` key to each ancestor that includes it's parents
134
+ # - we set a `structure` attribute if structure was returned in the
135
+ # response
136
+ #
137
+ def initialize(attributes, options = {})
138
+ super(attributes, options)
139
+
140
+ ancestors.each_with_object({}) do |ancestor, hsh|
141
+ hsh[ancestor.type] = ancestor.identifier
142
+ ancestor.attributes["hierarchy"] = hsh.dup
143
+ end
144
+
145
+ # force casting of structure to a structure object
146
+ # such that it gets cached immediately - see
147
+ # Structure sideloaded format for details
148
+ structure if attributes["structure"]
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,7 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class ApiDocumentation < Base
4
+ include DefaultDocumentationSetup
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class Base < Ecfr::Base
4
+ require_relative "api_documentation"
5
+ require_relative "status"
6
+
7
+ # note: structure must be required before ancestors
8
+ require_relative "structure"
9
+ require_relative "ancestors"
10
+ require_relative "title"
11
+ require_relative "xml_content"
12
+
13
+ SERVICE_PATH = "/api/versioner"
14
+
15
+ def self.base_url
16
+ Ecfr.config.versioner_service_url || Ecfr.config.base_url
17
+ end
18
+
19
+ def self.service_name
20
+ "Versioner Service"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class Status < Base
4
+ include DefaultStatusSetup
5
+ end
6
+ end
7
+ end