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,78 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class EcfrCorrection < Base
4
+ require_relative "ecfr_correction/cfr_reference"
5
+
6
+ result_key :ecfr_corrections
7
+
8
+ attribute :corrective_action,
9
+ desc: "description of action taken"
10
+ attribute :fr_citation,
11
+ desc: "Federal Register citation in form of '80 FR 12345'"
12
+
13
+ attribute :id,
14
+ type: :integer
15
+ attribute :position,
16
+ type: :integer,
17
+ desc: "position of correction in list"
18
+ attribute :title,
19
+ type: :integer,
20
+ desc: "CFR Title"
21
+ attribute :year,
22
+ type: :integer,
23
+ desc: "year of corrective action"
24
+
25
+ attribute :error_corrected,
26
+ type: :date,
27
+ desc: "date error was corrected"
28
+ attribute :error_occurred,
29
+ type: :date,
30
+ desc: "date error first occured"
31
+ attribute :last_modified,
32
+ type: :date,
33
+ desc: "date this correction was last modified"
34
+
35
+ attribute :cfr_references,
36
+ type: Array(CfrReference),
37
+ desc: "the portions of the CFR this correction applies to"
38
+
39
+ attribute :display_in_toc,
40
+ type: :boolean,
41
+ desc: "whether this correction should be displayed on table of contents pages"
42
+
43
+ CORRECTIONS_PATH = "v1/corrections.json"
44
+
45
+ #
46
+ # Retrieve the list of all corrections
47
+ #
48
+ # @return [[<EcfrCorrection>]] an array of eCFR correction records
49
+ #
50
+ def self.all
51
+ perform(
52
+ :get,
53
+ CORRECTIONS_PATH
54
+ )
55
+ end
56
+
57
+ #
58
+ # Retrieve a list of corrections filtered by the provided parameters
59
+ #
60
+ # @param [<Hash>] args parameters by which to filter the results; supported args:
61
+ # - :date
62
+ # - :error_corrected_date
63
+ # - :title
64
+ #
65
+ # @return [[<EcfrCorrection>]] an array of eCFR correction records
66
+ #
67
+ def self.where(args)
68
+ args.slice!(:date, :error_corrected_date, :title)
69
+
70
+ perform(
71
+ :get,
72
+ CORRECTIONS_PATH,
73
+ params: args
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,19 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class EditorialNote
4
+ class Hierarchy
5
+ include AttributeMethodDefinition
6
+ extend Extensible
7
+
8
+ attribute :title,
9
+ desc: "Title number"
10
+ attribute :part,
11
+ desc: "Part identifier"
12
+ attribute :subpart,
13
+ desc: "Subpart identifier"
14
+ attribute :section,
15
+ desc: "Section identifier"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class EditorialNote < Base
4
+ require_relative "editorial_note/hierarchy"
5
+
6
+ result_key :editorial_notes
7
+
8
+ attribute :editorial_note,
9
+ desc: "content of the editorial note"
10
+
11
+ attribute :end_date,
12
+ type: :date,
13
+ desc: "last date the editorial note applies to"
14
+ attribute :start_date,
15
+ type: :date,
16
+ desc: "first date the editorial note applies to"
17
+
18
+ attribute :hierarchies,
19
+ type: Array(Hierarchy),
20
+ desc: "the hierarchies of the content this editorial note applies to"
21
+
22
+ EDITORIAL_NOTES_PATH = "v1/editorial-notes"
23
+
24
+ #
25
+ # Retrieve a list of editorial notes
26
+ #
27
+ # @param [<Hash>] args parameters by which to filter the results; supported args:
28
+ # - :title
29
+ #
30
+ # @return [[<EditorialNote>]] an array of editorial note records
31
+ #
32
+ def self.where(args)
33
+ perform(
34
+ :get,
35
+ "#{EDITORIAL_NOTES_PATH}/title-#{args[:title]}.json"
36
+ )
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,17 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class IbrCfrRange
4
+ class Address
5
+ include AttributeMethodDefinition
6
+ extend Extensible
7
+
8
+ attribute :address,
9
+ desc: "generally a physical address"
10
+ attribute :address_group_1,
11
+ desc: "first level description of address"
12
+ attribute :address_group_2,
13
+ desc: "second level description of address"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,28 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class IbrCfrRange
4
+ class Organization
5
+ include AttributeMethodDefinition
6
+ extend Extensible
7
+
8
+ attribute :details,
9
+ desc: "generally contact information for the organization"
10
+ attribute :name,
11
+ desc: "organization name"
12
+
13
+ class Material
14
+ include AttributeMethodDefinition
15
+
16
+ attribute :cfr_references,
17
+ desc: "locations in the CFR where this material is incorporated by reference"
18
+ attribute :details,
19
+ desc: "information about what was incorporated - generally publication title, etc."
20
+ end
21
+
22
+ attribute :materials,
23
+ type: Array(Material),
24
+ desc: "array of materials that have been incorporated by reference"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class IbrCfrRange < Base
4
+ require_relative "ibr_cfr_range/address"
5
+ require_relative "ibr_cfr_range/organization"
6
+
7
+ result_key :ibr_cfr_ranges
8
+
9
+ attribute :label,
10
+ desc: "description - generally the agency responsible"
11
+ attribute :notes,
12
+ desc: "notes about the IBR content"
13
+ attribute :part_range,
14
+ desc: "the range of parts the IBR item applies to"
15
+
16
+ attribute :title,
17
+ type: :integer,
18
+ desc: "CFR title number"
19
+
20
+ attribute :organizations,
21
+ type: Array(Organization),
22
+ desc: "array of organizations responsible for IBR materials and more details"
23
+
24
+ attribute :addresses,
25
+ type: Array(Address),
26
+ desc: "address information for district offices, etc."
27
+
28
+ IBR_CFR_RANGES_PATH = "v1/incorporation-by-reference"
29
+
30
+ #
31
+ # Retrieve a summarized list of all IBR information
32
+ #
33
+ # @return [[<IbrCfrRange>]] an array of IBR information
34
+ # limited to `title`, `part_range`, and `label` fields
35
+ #
36
+ def self.all
37
+ perform(
38
+ :get,
39
+ "#{IBR_CFR_RANGES_PATH}.json"
40
+ )
41
+ end
42
+
43
+ #
44
+ # Retrieve a detailed list of IBR information for the request IBR CFR range
45
+ #
46
+ # @param [<String, Integer>] title CFR title
47
+ # @param [<String>] part_range the part range in the form "1-99"
48
+ #
49
+ # @return [<IbrCfrRange>] contains all data available for specified params
50
+ #
51
+ def self.find(title, part_range)
52
+ perform(
53
+ :get,
54
+ ibr_cfr_range_path(title, part_range),
55
+ perform_options: {attributes_key: "ibr_cfr_range"}
56
+ )
57
+ end
58
+
59
+ def self.ibr_cfr_range_path(title, part_range)
60
+ part_range = ERB::Util.url_encode(part_range)
61
+
62
+ "#{IBR_CFR_RANGES_PATH}/title-#{title}/#{part_range}.json"
63
+ end
64
+ private_class_method :ibr_cfr_range_path
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,19 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class Issue
4
+ class Change
5
+ extend Ecfr::Constants::ChangeTypes
6
+
7
+ include AttributeMethodDefinition
8
+ extend Extensible
9
+
10
+ attribute :change_description,
11
+ desc: "CFR references that this change affects"
12
+
13
+ attribute :change_type,
14
+ type: :symbol,
15
+ desc: "indicates the type of change - corresponds to the keys in {KNOWN_CHANGE_TYPES}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,86 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class Issue < Base
4
+ require_relative "issue/change"
5
+
6
+ result_key :issues
7
+
8
+ attribute :description,
9
+ desc: "US standard date description"
10
+
11
+ attribute :titles,
12
+ type: Array(:integer),
13
+ desc: "array of CFR title numbers affected in this issue"
14
+
15
+ attribute :end_date,
16
+ type: :date,
17
+ desc: "end date of issue"
18
+ attribute :issue_date,
19
+ type: :date,
20
+ desc: "date of issue"
21
+ attribute :start_date,
22
+ type: :date,
23
+ desc: "start date of issue"
24
+
25
+ attribute :changes,
26
+ type: Array(Change),
27
+ desc: "array of change types and CFR reference data"
28
+
29
+ metadata_key :meta
30
+
31
+ # @return [String, nil]
32
+ metadata :next,
33
+ desc: "date of next issue in ISO format"
34
+ # @return [String, nil]
35
+ metadata :previous,
36
+ desc: "date of previous issue in ISO format"
37
+
38
+ ISSUES_PATH = "v1/issues"
39
+
40
+ #
41
+ # Retrieves a summary of all available issues
42
+ #
43
+ # @return [[<Issue>]] does not include the `changes` or `metadata` items
44
+ #
45
+ def self.all
46
+ perform(
47
+ :get,
48
+ "#{ISSUES_PATH}.json"
49
+ )
50
+ end
51
+
52
+ #
53
+ # Retrieves detailed issue data for the specified date.
54
+ # If the isssue spans multiple days and date in that timespan will return the issue
55
+ #
56
+ # @param [<Date, DateTime, Time, String<'current'>] date the string 'current' can be provided to retrieve the newest issue at time of request
57
+ #
58
+ # @return [[<Issue>]] returns array containing single issue for the date provided
59
+ #
60
+ def self.find(date:)
61
+ perform(
62
+ :get,
63
+ issues_path(date)
64
+ )
65
+ end
66
+
67
+ def self.issues_path(date)
68
+ "#{ISSUES_PATH}/#{date_slug_for(date)}"
69
+ end
70
+
71
+ # supports :current, Date, DateTime, Time
72
+ def self.date_slug_for(date_like)
73
+ return "current" if date_like == :current
74
+
75
+ if date_like.respond_to?(:to_date)
76
+ date_like = date_like.to_date
77
+ end
78
+
79
+ return date_like.iso8601 if date_like.is_a?(Date)
80
+
81
+ raise "Unknown input for Ecfr::AdminService::Issue"
82
+ end
83
+ private_class_method :date_slug_for
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,34 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class SiteNotification < Base
4
+ result_key :notifications
5
+
6
+ attribute :last_updated_by,
7
+ desc: "email of user who last updated the notification"
8
+ attribute :message,
9
+ desc: "notification content"
10
+ attribute :notification_type,
11
+ desc: "where the notification should be displayed; either `global` or `global_top`"
12
+
13
+ attribute :active,
14
+ type: :boolean
15
+
16
+ attribute :updated_at,
17
+ type: :datetime
18
+
19
+ SITE_NOTIFICATIONS_PATH = "v1/notifications.json"
20
+
21
+ #
22
+ # Retrieves all active notifications
23
+ #
24
+ # @return [[<SiteNotification>]] array of site notifications
25
+ #
26
+ def self.all
27
+ perform(
28
+ :get,
29
+ SITE_NOTIFICATIONS_PATH
30
+ )
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ module Ecfr
2
+ module AdminService
3
+ class Status < Base
4
+ include DefaultStatusSetup
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,72 @@
1
+ module Ecfr
2
+ class AttributeCaster
3
+ #
4
+ # Given a value and a type return the value cast to the type.
5
+ # Some types do not actually modify the value but exist in order
6
+ # to support automated documentation via YARD.
7
+ #
8
+ # @param [<Integer, Float, String>] val the value as
9
+ # parsed from a JSON response
10
+ # @param [<Symbol, Class>] type a type to cast the value to,
11
+ # classes will be instantiated with the value
12
+ # @param [<Hash>] options options are passed to classes when they
13
+ # are instantiated. {base: false} is merged into these
14
+ # options to indicate that metadata attributes (among other
15
+ # things) should not be populated. See {Ecfr::Base#initialize}
16
+ #
17
+ # @return a typecast value
18
+ #
19
+ # @note some API endpoints do not return a key for values that are
20
+ # false (such as processing_in_progress). Because the attribute
21
+ # is not include in the response it can not be typecast as
22
+ # boolean false (it will return nil however which is still falsey)
23
+ #
24
+ def self.cast_attr(val, type, options = {})
25
+ return val if val.nil?
26
+
27
+ case type
28
+ when :integer
29
+ val # JSON parse was sufficient
30
+ when :float
31
+ val # JSON parse was sufficient
32
+ when :boolean
33
+ ActiveModel::Type::Boolean.new.cast(val)
34
+ when :date
35
+ return val if val.is_a?(Date)
36
+ Date.parse(val.to_s)
37
+ when :datetime
38
+ return val if val.is_a?(DateTime)
39
+ DateTime.parse(val.to_s)
40
+ when :symbol
41
+ val.to_sym
42
+ when ->(type) { type.respond_to?(:new) }
43
+ instantiate(type, val, options.merge(base: false))
44
+ # type.new(val, options.merge(base: false))
45
+ when ->(type) { type.is_a?(Array) && type.first.respond_to?(:new) }
46
+ val.map { |v| instantiate(type.first, v, options.merge(base: false)) } # type.first.new(v, options.merge(base: false)) }
47
+ when ->(type) { type.is_a?(Array) && type.first == :symbol }
48
+ val.map(&:to_sym)
49
+ when ->(type) { type.is_a?(Array) && [:integer, :string].include?(type.first) }
50
+ val # JSON parse was sufficient
51
+ when nil
52
+ val # JSON parse was sufficient
53
+ else
54
+ raise "Undefined type '#{type}' provided to 'attribute'/'metadata'!"
55
+ end
56
+ end
57
+
58
+ #
59
+ # Support inheritance by instantiating the class
60
+ # inheriting from our internal class if it's been
61
+ # defined
62
+ #
63
+ def self.instantiate(klass, val, options)
64
+ if const_defined?("#{klass}::KLASS")
65
+ klass::KLASS.new(val, options)
66
+ else
67
+ klass.new(val, options)
68
+ end
69
+ end
70
+ private_class_method :instantiate
71
+ end
72
+ end
@@ -0,0 +1,92 @@
1
+ module Ecfr
2
+ #
3
+ # Provides the base functionality for attribute and metadata
4
+ # definition.
5
+ #
6
+ module AttributeMethodDefinition
7
+ module ClassMethods
8
+ #
9
+ # Provides attribute definition. The attribute defined is
10
+ # expected to be part of the response. If the key in the
11
+ # response is different than the attribute name (eg you want
12
+ # to rename it) then the `value_key` option can be used.
13
+ #
14
+ # When the :type option is :boolean we also define a
15
+ # predicate method (eg: we define both foo and foo?)
16
+ #
17
+ # @param [<Symbol>] attr name of the attribute to define
18
+ # @param [<Hash>] options
19
+ # @option options [<Symbol, Class>] type a type to cast the
20
+ # JSON parsed value to. See {Ecfr::AttributeCaster}.
21
+ # @option options [<String>] desc
22
+ # @option options [<String>] value_key the name of the key
23
+ # in the repsonse (only needed if the attribute name is
24
+ # different than the key in the response)
25
+ # @option options [<Hash>] options options to be passed to
26
+ # Class when it is instantiated (when :type is a class)
27
+ #
28
+ # @note If no type option is provided, String, is assumed by
29
+ # the YARD documentation generated. Types other than String
30
+ # are expected to be explicitely defined.
31
+ #
32
+ def attribute(attr, options = {})
33
+ define_method attr do
34
+ val = extract_value(attr, options)
35
+
36
+ # sideloaded classes need their referrer to properly
37
+ # cache themselves
38
+ if options[:options] && options[:options][:self]
39
+ options[:options][:referrer] = self
40
+ end
41
+
42
+ Ecfr::AttributeCaster.cast_attr(val, options[:type], options[:options] || {})
43
+ end
44
+
45
+ if options[:type] == :boolean
46
+ define_method "#{attr}?" do
47
+ val = extract_value(attr, options)
48
+ Ecfr::AttributeCaster.cast_attr(val, options[:type], options[:options] || {})
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def self.included(base)
55
+ base.instance_eval do
56
+ attr_reader :attributes, :options
57
+ end
58
+
59
+ base.extend(ClassMethods)
60
+ end
61
+
62
+ def initialize(attributes = {}, options = {})
63
+ @attributes = attributes
64
+ @options = options
65
+ end
66
+
67
+ private
68
+
69
+ #
70
+ # Returns the value for the requested attribute from the
71
+ # set of attributes. A value_key option can be passed
72
+ # for attributes that don't match the key in the response
73
+ # Return nil if no attributes are present at all (in the
74
+ # case of metadata this means the key wasn't present in the
75
+ # response)
76
+ #
77
+ # @param [Symbol, String] attr attribute name
78
+ # @param [Hash] options
79
+ # @option options [Symbol, String] value_key the key in the
80
+ # response to retreive instead of the attribute name
81
+ #
82
+ def extract_value(attr, options)
83
+ return nil unless @attributes
84
+
85
+ if options[:value_key]
86
+ @attributes[options[:value_key].to_s]
87
+ else
88
+ @attributes[attr.to_s]
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/ecfr/base.rb ADDED
@@ -0,0 +1,71 @@
1
+ module Ecfr
2
+ class Base < Client
3
+ include AttributeMethodDefinition
4
+ include Enumerable
5
+ extend Extensible
6
+
7
+ class_attribute :result_root
8
+ class_attribute :metadata_accessor
9
+ self.metadata_accessor = "metadata"
10
+
11
+ def self.result_key(key)
12
+ self.result_root = key.to_s
13
+ end
14
+
15
+ def self.metadata_key(key)
16
+ self.metadata_accessor = key.to_s
17
+ end
18
+
19
+ def self.basic_auth_client_options
20
+ {
21
+ basic_auth: {
22
+ username: Ecfr.config.ecfr_basic_auth_username,
23
+ password: Ecfr.config.ecfr_basic_auth_password
24
+ }
25
+ }
26
+ end
27
+
28
+ def self.metadata(*metadata)
29
+ Metadata.attribute(*metadata)
30
+ end
31
+
32
+ attr_reader :metadata, :results, :response_status, :request_data
33
+
34
+ SUPPORTED_ARRAY_ACCESSORS = %i[empty? first last size]
35
+ delegate(*SUPPORTED_ARRAY_ACCESSORS, to: :results)
36
+ alias_method :all, :results
37
+
38
+ def initialize(attributes = {}, options = {})
39
+ default_options = {base: true}
40
+ options = default_options.merge(options)
41
+
42
+ @response_status = options.delete(:response_status)
43
+ @request_data = options.delete(:request_data)
44
+
45
+ # - handle result_root only if the first time instantiating
46
+ # the class from the results
47
+ # - some responses won't be a parsed attribute hash
48
+ if options[:base] && attributes.is_a?(Hash)
49
+ @metadata = Metadata.new(attributes.delete(self.class.metadata_accessor))
50
+
51
+ if result_root && attributes[result_root]
52
+ @results = attributes[result_root].map do |r|
53
+ self.class.new(r, base: false)
54
+ end
55
+ end
56
+ end
57
+
58
+ super(attributes)
59
+ end
60
+
61
+ def each
62
+ @results.each { |result| yield result }
63
+ end
64
+
65
+ class Metadata
66
+ include AttributeMethodDefinition
67
+
68
+ attr_reader :attributes
69
+ end
70
+ end
71
+ end