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,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