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