ecfr 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +11 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rspec_parallel +4 -0
- data/.rubocop.yml +28 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +6 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +138 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/ecfr.gemspec +74 -0
- data/lib/ecfr/admin_service/agency/hierarchy.rb +27 -0
- data/lib/ecfr/admin_service/agency.rb +34 -0
- data/lib/ecfr/admin_service/api_documentation.rb +7 -0
- data/lib/ecfr/admin_service/base.rb +26 -0
- data/lib/ecfr/admin_service/build.rb +38 -0
- data/lib/ecfr/admin_service/ecfr_correction/cfr_reference.rb +17 -0
- data/lib/ecfr/admin_service/ecfr_correction.rb +78 -0
- data/lib/ecfr/admin_service/editorial_note/hierarchy.rb +19 -0
- data/lib/ecfr/admin_service/editorial_note.rb +40 -0
- data/lib/ecfr/admin_service/ibr_cfr_range/address.rb +17 -0
- data/lib/ecfr/admin_service/ibr_cfr_range/organization.rb +28 -0
- data/lib/ecfr/admin_service/ibr_cfr_range.rb +67 -0
- data/lib/ecfr/admin_service/issue/change.rb +19 -0
- data/lib/ecfr/admin_service/issue.rb +86 -0
- data/lib/ecfr/admin_service/site_notification.rb +34 -0
- data/lib/ecfr/admin_service/status.rb +7 -0
- data/lib/ecfr/attribute_caster.rb +72 -0
- data/lib/ecfr/attribute_method_definition.rb +92 -0
- data/lib/ecfr/base.rb +71 -0
- data/lib/ecfr/client.rb +318 -0
- data/lib/ecfr/common/hierarchy.rb +35 -0
- data/lib/ecfr/configuration.rb +58 -0
- data/lib/ecfr/constants.rb +21 -0
- data/lib/ecfr/default_documentation_setup.rb +39 -0
- data/lib/ecfr/default_status_setup.rb +46 -0
- data/lib/ecfr/diff_service/base.rb +17 -0
- data/lib/ecfr/diff_service/status.rb +34 -0
- data/lib/ecfr/extensible.rb +45 -0
- data/lib/ecfr/facet_attribute_method_definition.rb +47 -0
- data/lib/ecfr/faraday/user_agent/middleware.rb +14 -0
- data/lib/ecfr/ofr_profile_service/base.rb +20 -0
- data/lib/ecfr/ofr_profile_service/status.rb +7 -0
- data/lib/ecfr/parallel_client.rb +33 -0
- data/lib/ecfr/prince_xml_service/base.rb +17 -0
- data/lib/ecfr/prince_xml_service/pdf.rb +31 -0
- data/lib/ecfr/renderer_service/base.rb +31 -0
- data/lib/ecfr/renderer_service/content.rb +34 -0
- data/lib/ecfr/renderer_service/diff.rb +31 -0
- data/lib/ecfr/renderer_service/origin.rb +56 -0
- data/lib/ecfr/renderer_service/status.rb +7 -0
- data/lib/ecfr/request_representation.rb +12 -0
- data/lib/ecfr/search_service/api_documentation.rb +7 -0
- data/lib/ecfr/search_service/base.rb +23 -0
- data/lib/ecfr/search_service/content_version/count.rb +33 -0
- data/lib/ecfr/search_service/content_version/hierarchical_count.rb +17 -0
- data/lib/ecfr/search_service/content_version/hierarchical_count_node.rb +30 -0
- data/lib/ecfr/search_service/content_version/hierarchichal_result.rb +42 -0
- data/lib/ecfr/search_service/content_version/result.rb +110 -0
- data/lib/ecfr/search_service/content_version/suggestion.rb +76 -0
- data/lib/ecfr/search_service/content_version/summary.rb +27 -0
- data/lib/ecfr/search_service/content_version.rb +85 -0
- data/lib/ecfr/search_service/date_facet.rb +19 -0
- data/lib/ecfr/search_service/facet_base.rb +55 -0
- data/lib/ecfr/search_service/status.rb +7 -0
- data/lib/ecfr/search_service/title_facet.rb +18 -0
- data/lib/ecfr/subscriptions_service/base.rb +19 -0
- data/lib/ecfr/subscriptions_service/status.rb +7 -0
- data/lib/ecfr/subscriptions_service/subscription.rb +97 -0
- data/lib/ecfr/testing/extensions/admin_service/ecfr_correction_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/admin_service/issue_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/renderer_service/origin_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/search_service/content_version_result_extensions.rb +16 -0
- data/lib/ecfr/testing/extensions/search_service/date_facet_extensions.rb +13 -0
- data/lib/ecfr/testing/extensions/versioner_service/ancestors_extensions.rb +20 -0
- data/lib/ecfr/testing/extensions/versioner_service/title_extenstions.rb +16 -0
- data/lib/ecfr/testing/factories/admin_service/cfr_reference_factory.rb +14 -0
- data/lib/ecfr/testing/factories/admin_service/ecfr_correction_factory.rb +31 -0
- data/lib/ecfr/testing/factories/admin_service/issue_change_factory.rb +12 -0
- data/lib/ecfr/testing/factories/admin_service/issue_factory.rb +21 -0
- data/lib/ecfr/testing/factories/common/hierarchy_factory.rb +36 -0
- data/lib/ecfr/testing/factories/renderer_service/origin_factory.rb +32 -0
- data/lib/ecfr/testing/factories/search_service/content_version_count_factory.rb +20 -0
- data/lib/ecfr/testing/factories/search_service/content_version_result_factory.rb +76 -0
- data/lib/ecfr/testing/factories/search_service/date_facet_factory.rb +12 -0
- data/lib/ecfr/testing/factories/versioner_service/ancestors_factory.rb +26 -0
- data/lib/ecfr/testing/factories/versioner_service/metadata_node_info_factory.rb +15 -0
- data/lib/ecfr/testing/factories/versioner_service/node_summary_factory.rb +16 -0
- data/lib/ecfr/testing/factories/versioner_service/structure_factory.rb +57 -0
- data/lib/ecfr/testing/factories/versioner_service/title_factory.rb +36 -0
- data/lib/ecfr/testing/factory_bot_helpers/content_version.rb +38 -0
- data/lib/ecfr/testing/factory_bot_helpers/ecfr_gem_initialize_helpers.rb +51 -0
- data/lib/ecfr/testing/helpers/response_helper.rb +5 -0
- data/lib/ecfr/testing/strategies/ecfr_attribute_hash_strategy.rb +37 -0
- data/lib/ecfr/testing.rb +28 -0
- data/lib/ecfr/version.rb +5 -0
- data/lib/ecfr/versioner_service/ancestors/metadata_node_info.rb +22 -0
- data/lib/ecfr/versioner_service/ancestors/node_summary.rb +54 -0
- data/lib/ecfr/versioner_service/ancestors.rb +152 -0
- data/lib/ecfr/versioner_service/api_documentation.rb +7 -0
- data/lib/ecfr/versioner_service/base.rb +24 -0
- data/lib/ecfr/versioner_service/status.rb +7 -0
- data/lib/ecfr/versioner_service/structure.rb +120 -0
- data/lib/ecfr/versioner_service/title.rb +78 -0
- data/lib/ecfr/versioner_service/xml_content.rb +59 -0
- data/lib/ecfr.rb +90 -0
- data/lib/yard/attribute_handler.rb +87 -0
- data/lib/yard/metadata_handler.rb +87 -0
- 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,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
|