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,120 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ class Structure < Base
4
+ STRUCTURE_PATH = "v1/structure"
5
+
6
+ #
7
+ # Retrieves the structure content for given date and hierarchy
8
+ #
9
+ # @param [<Date, String, 'current'>] date ISO date string or 'current'
10
+ # @param [<Integer, String>] title_number CFR title number
11
+ # @param [<Hash>] options
12
+ # @option options [String] format (json) the format that should be returned
13
+ #
14
+ # @return [<Structure>] a structure representation based on the format requested
15
+ #
16
+ def self.find(date, title_number, options = {})
17
+ default_options = {format: "json"}
18
+ options = default_options.merge(options.symbolize_keys)
19
+
20
+ format = options.delete(:format)
21
+
22
+ perform(
23
+ :get,
24
+ structure_path(date, title_number, format),
25
+ params: options.except(:section, :appendix),
26
+ perform_options: {
27
+ init_data: {format: format},
28
+ parse_response: false
29
+ }
30
+ )
31
+ end
32
+
33
+ #
34
+ # Provides a url to the structure content for given date and hierarchy
35
+ #
36
+ # @param [<Date, String, 'current'>] date ISO date string or 'current'
37
+ # @param [<Integer, String>] title_number CFR title number
38
+ # @param [<Hash>] options <description>
39
+ # @option options [String] format (json) the format that should be returned
40
+ #
41
+ # @return [<String>] URL to retreive structure content for given date and hierarchy
42
+ #
43
+ def self.url_for(date, title_number, options = {})
44
+ default_options = {format: "json"}
45
+ options = default_options.merge(options.symbolize_keys)
46
+
47
+ format = options.delete(:format)
48
+ path = [self::SERVICE_PATH, structure_path(date, title_number, format)].join("/")
49
+
50
+ client.build_url(path, options).to_s
51
+ end
52
+
53
+ def self.structure_path(date, title_number, format = "json")
54
+ "#{STRUCTURE_PATH}/#{date}/title-#{title_number}.#{format}"
55
+ end
56
+ private_class_method :structure_path
57
+
58
+ # @!attribute data [Hash, Nokogiri::XML::Document]
59
+ attr_reader :data
60
+
61
+ def initialize(data, options = {format: "json"})
62
+ data = data.body if data.is_a?(Faraday::Response)
63
+
64
+ format = options[:format]
65
+ skip_cache = false
66
+
67
+ # we're going to load a response from sideloaded cache
68
+ if data.is_a?(Hash) && format != "sideloaded"
69
+ format = "sideloaded"
70
+ skip_cache = true
71
+ end
72
+
73
+ if format == "json"
74
+ @data = JSON.parse(data).with_indifferent_access
75
+ elsif format == "xml"
76
+ @data = Nokogiri::XML::Document.parse(data)
77
+ elsif format == "sideloaded"
78
+ @data = data.with_indifferent_access
79
+
80
+ if Ecfr.config.cache_responses && !skip_cache
81
+ # cache sideloaded response seperately so that a non sideloaded
82
+ # call to structure can take advantage of cache
83
+ cache_key = determine_cache_key(options[:referrer].request_data)
84
+
85
+ # the client expects to be dealing with response objects, in a
86
+ # sideload scenario we need to cache a response like object
87
+ RequestStore[cache_key] = OpenStruct.new(
88
+ body: data,
89
+ status: options[:referrer].response_status
90
+ )
91
+ end
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ #
98
+ # Returns a cache key that would be the same as if this class
99
+ # wasn't being sideloaded. Currently this only supports
100
+ # sideloading from the Ancestors endpoint.
101
+ #
102
+ # @param [<Hash>] request_data contains information about the
103
+ # original ancestor request data
104
+ #
105
+ # @return [<String>] cache_key
106
+ #
107
+ def determine_cache_key(request_data)
108
+ split = request_data[:path].split("/")
109
+ date = split[-2]
110
+ title_number = split[-1].split("-")[1]
111
+
112
+ path = "#{self.class::SERVICE_PATH}/#{self.class.send(:structure_path, date, title_number)}"
113
+
114
+ params = request_data[:params].except(:structure, :metadata)
115
+
116
+ self.class.cache_key(request_data[:method], path, params)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,78 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ #
4
+ # The titles API endpoint provides information about the status
5
+ # of each title and the overall system processing status.
6
+ #
7
+ # At most times the up_to_date_as_of date will be the same for
8
+ # all titles. Only when the system import is in process (each
9
+ # evening) or if reprocessing is being performed will these dates
10
+ # differ.
11
+ #
12
+ # If any title is currently importing or being reprocessed the
13
+ # processing_in_progress attribute will be set.
14
+ #
15
+ # The lastest_amended_on indicates the last time the Title
16
+ # has a substantive change occur. The latest_issue_date is the
17
+ # most recent date that we received the title for processing.
18
+ # These two attributes will generally only differ when we
19
+ # receive non-sunstantive updates (changes to fix minor spelling,
20
+ # punctuation, etc.).
21
+ #
22
+ class Title < Base
23
+ result_key :titles
24
+
25
+ attribute :name,
26
+ desc: "name of Title"
27
+
28
+ attribute :number,
29
+ type: :integer,
30
+ desc: "Title number"
31
+
32
+ attribute :processing_in_progress,
33
+ type: :boolean,
34
+ desc: "whether the Title is currently being processed"
35
+ attribute :reserved,
36
+ type: :boolean,
37
+ desc: "whether the Title is Reserved"
38
+
39
+ attribute :latest_amended_on,
40
+ type: :date,
41
+ desc: "the most recent date that the Title has been amended on"
42
+ attribute :latest_issue_date,
43
+ type: :date,
44
+ desc: "the most recent issue of the Title"
45
+ attribute :up_to_date_as_of,
46
+ type: :date,
47
+ desc: "the date through which the Title considered curent"
48
+
49
+ metadata_key :meta
50
+
51
+ metadata :date,
52
+ type: :date,
53
+ desc: "the max up to date value of all Titles"
54
+ metadata :import_in_progress,
55
+ type: :boolean,
56
+ desc: "whether any Title is undergoing processing"
57
+
58
+ TITLES_PATH = "v1/titles"
59
+
60
+ #
61
+ # Retreive the list of all Titles
62
+ #
63
+ # @param [<Hash>] options
64
+ # @option options [String] :build_id internal use only - used
65
+ # to retreive data about a specific build
66
+ #
67
+ # @return [<Title>] array of Titles with data
68
+ #
69
+ def self.all(options = {})
70
+ perform(
71
+ :get,
72
+ TITLES_PATH,
73
+ params: options.compact
74
+ )
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,59 @@
1
+ module Ecfr
2
+ module VersionerService
3
+ #
4
+ # The XML endpoint will return the full XML available
5
+ # for a given date and hierarchy.
6
+ #
7
+ # This is used primarily by our internal import processes
8
+ # and to provide links to XML from the web UI.
9
+ #
10
+ class XmlContent < Base
11
+ attribute :xml,
12
+ desc: "XML document"
13
+
14
+ XML_PATH = "v1/full"
15
+
16
+ #
17
+ # Retreive the XML for a given date and hierarchy
18
+ #
19
+ # @param [<Date, String, 'current'>] date ISO string or 'current'
20
+ # @param [<Integer, String>] title_number the title of interest
21
+ # @param [<Hash>] options a hash of hierarchy levels for the content desired - see attributes defined in {Ecfr::Common::Hierarchy} for acceptable keys
22
+ # @option options [<String>] build_id internal use only - a specific build id
23
+ #
24
+ # @return [<XML>] XML for the full title or pared down to the requested hierarchy
25
+ #
26
+ def self.find(date, title_number, options = {})
27
+ new(
28
+ {
29
+ xml: get(
30
+ xml_content_path(date, title_number),
31
+ options
32
+ ).body
33
+ }.stringify_keys
34
+ )
35
+ end
36
+
37
+ #
38
+ # Provides a url to the XML content for a given set of parameters
39
+ #
40
+ # @param [<Date, String, 'current'>] date ISO string or 'current'
41
+ # @param [<Integer, String>] title_number the title of interest
42
+ # @param [<Hash>] options a hash of hierarchy levels for the content desired - see attributes defined in {Ecfr:Common::Hierarchy} for acceptable keys
43
+ # @option options [<String>] build_id internal use only - a specific build id
44
+ #
45
+ # @return [<String>] URL to retreive XML content for the given parameters
46
+ #
47
+ def self.url_for(date, title_number, options = {})
48
+ path = xml_content_path(date, title_number)
49
+
50
+ client.build_url(path, options).to_s
51
+ end
52
+
53
+ def self.xml_content_path(date, title_number)
54
+ "#{SERVICE_PATH}/#{XML_PATH}/#{date}/title-#{title_number}.xml"
55
+ end
56
+ private_class_method :xml_content_path
57
+ end
58
+ end
59
+ end
data/lib/ecfr.rb ADDED
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "active_model/type"
5
+
6
+ require "active_support"
7
+ require "active_support/core_ext/class/attribute"
8
+ require "active_support/core_ext/hash"
9
+ require "active_support/core_ext/module/delegation"
10
+ require "active_support/core_ext/string"
11
+
12
+ require "faraday"
13
+ require "faraday/net_http_persistent"
14
+ require_relative "ecfr/faraday/user_agent/middleware"
15
+
16
+ # NOTE: faraday-typhoeus must be installed by consuming
17
+ # applications
18
+ begin
19
+ Gem::Specification.find_by_name("faraday-typhoeus")
20
+ require "faraday/typhoeus"
21
+ rescue Gem::LoadError
22
+ end
23
+
24
+ # NOTE: nokogiri must be installed by consuming
25
+ # applications
26
+ begin
27
+ Gem::Specification.find_by_name("nokogiri")
28
+ require "nokogiri"
29
+ rescue Gem::LoadError
30
+ end
31
+
32
+ # NOTE: request_store must be installed by consuming
33
+ # applications
34
+ begin
35
+ Gem::Specification.find_by_name("request_store")
36
+ require "request_store"
37
+ rescue Gem::LoadError
38
+ end
39
+
40
+ module Ecfr
41
+ def self.config
42
+ @config ||= Configuration.new
43
+ end
44
+
45
+ def self.configure
46
+ yield(config)
47
+ config.validate!
48
+ end
49
+
50
+ def self.services
51
+ [
52
+ Ecfr::AdminService,
53
+ Ecfr::DiffService,
54
+ Ecfr::OfrProfileService,
55
+ Ecfr::PrinceXmlService,
56
+ Ecfr::RendererService,
57
+ Ecfr::SearchService,
58
+ Ecfr::SubscriptionsService,
59
+ Ecfr::VersionerService
60
+ ]
61
+ end
62
+ end
63
+
64
+ # order important
65
+ require_relative "ecfr/version"
66
+ require_relative "ecfr/configuration"
67
+ require_relative "ecfr/constants"
68
+ require_relative "ecfr/extensible"
69
+
70
+ require_relative "ecfr/attribute_caster"
71
+ require_relative "ecfr/attribute_method_definition"
72
+ require_relative "ecfr/facet_attribute_method_definition"
73
+
74
+ require_relative "ecfr/common/hierarchy"
75
+ require_relative "ecfr/default_documentation_setup"
76
+ require_relative "ecfr/default_status_setup"
77
+ require_relative "ecfr/parallel_client"
78
+
79
+ require_relative "ecfr/client"
80
+ require_relative "ecfr/base"
81
+ require_relative "ecfr/request_representation"
82
+
83
+ require_relative "ecfr/admin_service/base"
84
+ require_relative "ecfr/diff_service/base"
85
+ require_relative "ecfr/ofr_profile_service/base"
86
+ require_relative "ecfr/prince_xml_service/base"
87
+ require_relative "ecfr/renderer_service/base"
88
+ require_relative "ecfr/search_service/base"
89
+ require_relative "ecfr/subscriptions_service/base"
90
+ require_relative "ecfr/versioner_service/base"
@@ -0,0 +1,87 @@
1
+ class AttributeHandler < YARD::Handlers::Ruby::AttributeHandler
2
+ handles method_call(:attribute)
3
+ namespace_only
4
+
5
+ def process
6
+ params = statement.parameters(false).dup
7
+ options = params.pop if params.last.type == :list
8
+
9
+ validated_attribute_names(params).each do |name|
10
+ namespace.attributes[scope][name] ||= SymbolHash[:read => nil, :write => nil]
11
+
12
+ object = MethodObject.new(namespace, name, scope)
13
+ object.file = statement.file
14
+ object.source = statement.source
15
+ register(object)
16
+
17
+ if options.first.jump(:label).source == "type:"
18
+ return_type = options.first.jump(:const).source
19
+ end
20
+
21
+ return_type = "String"
22
+ desc = ""
23
+
24
+ options.each do |option|
25
+ if option.jump(:label).source == "desc:"
26
+ desc = option.jump(:string_content).source
27
+ end
28
+
29
+ if option.jump(:label).source == "type:"
30
+ if node_exists?(option, [:var_ref, :const])
31
+ return_type = option.source.sub(/^type: /, "")
32
+ elsif node_exists?(option, [:assoc, :symbol_literal])
33
+ return_type = walk(option, [:assoc, :symbol_literal, :ident]).source.capitalize
34
+ end
35
+
36
+ if node_exists?(option, [:arg_paren])
37
+ return_type = "[#{return_type.sub(/\AArray\(/,"").sub(/\)\z/, "")}]"
38
+ end
39
+ end
40
+ end
41
+
42
+ object.docstring = desc.gsub(/\n/, " ") if desc
43
+
44
+ # tags must be added after the docstring
45
+ object.add_tag(
46
+ YARD::Tags::Tag.new(:return, nil, return_type)
47
+ )
48
+
49
+ # Register the object explicitly
50
+ namespace.attributes[scope][name][:read] = object
51
+ end
52
+ end
53
+
54
+ #
55
+ # Checks whether a node exists at the given jumps
56
+ #
57
+ # @param [<AstNode>] ast_node A YARD::Parser::Ruby::AstNode
58
+ # @param [[<Symbol>]] jumps array of AST node types that should match
59
+ #
60
+ # @return [<Type>] <description>
61
+ #
62
+ def node_exists?(ast_node, jumps)
63
+ jumped = walk(ast_node, jumps)
64
+ !!jumped
65
+ end
66
+
67
+ #
68
+ # Recusively `jump`s the AstNode node to find a node that
69
+ # matches all provided jumps. Returns nil if node is
70
+ # not found.
71
+ #
72
+ # In constast `jump` will return any node that matches any
73
+ # of the provided jumps and return the original node if there
74
+ # are no matches.
75
+ #
76
+ # @param [<AstNode>] ast_node A YARD::Parser::Ruby::AstNode
77
+ # @param [[<Symbol>]] jumps through array of AST node types to walk
78
+ #
79
+ # @return [<AstNode, nil>] the requested node or nil
80
+ #
81
+ def walk(ast_node, jumps)
82
+ jumps = Array(jumps)
83
+ node = jumps.inject(ast_node) { |o, j| o.jump(j) }
84
+
85
+ return node if node != ast_node
86
+ end
87
+ end
@@ -0,0 +1,87 @@
1
+ class MetadataHandler < YARD::Handlers::Ruby::AttributeHandler
2
+ handles method_call(:metadata)
3
+ namespace_only
4
+
5
+ def process
6
+ params = statement.parameters(false).dup
7
+ options = params.pop if params.last.type == :list
8
+
9
+ validated_attribute_names(params).each do |name|
10
+ namespace.attributes[scope][name] ||= SymbolHash[:read => nil, :write => nil]
11
+
12
+ object = MethodObject.new(namespace, name, scope)
13
+ object.file = statement.file
14
+ object.source = statement.source
15
+ register(object)
16
+
17
+ if options.first.jump(:label).source == "type:"
18
+ return_type = options.first.jump(:const).source
19
+ end
20
+
21
+ return_type = "String"
22
+ desc = ""
23
+
24
+ options.each do |option|
25
+ if option.jump(:label).source == "desc:"
26
+ desc = option.jump(:string_content).source
27
+ end
28
+
29
+ if option.jump(:label).source == "type:"
30
+ if node_exists?(option, [:var_ref, :const])
31
+ return_type = option.source.sub(/^type: /, "")
32
+ elsif node_exists?(option, [:assoc, :symbol_literal])
33
+ return_type = walk(option, [:assoc, :symbol_literal, :ident]).source.capitalize
34
+ end
35
+
36
+ if node_exists?(option, [:arg_paren])
37
+ return_type = "[#{return_type.sub(/\AArray\(/,"").sub(/\)\z/, "")}]"
38
+ end
39
+ end
40
+ end
41
+
42
+ object.docstring = desc if desc
43
+
44
+ # tags must be added after the docstring
45
+ object.add_tag(
46
+ YARD::Tags::Tag.new(:return, nil, return_type)
47
+ )
48
+
49
+ # Register the object explicitly
50
+ namespace.attributes[scope][name][:read] = object
51
+ end
52
+ end
53
+
54
+ #
55
+ # Checks whether a node exists at the given jumps
56
+ #
57
+ # @param [<AstNode>] ast_node A YARD::Parser::Ruby::AstNode
58
+ # @param [[<Symbol>]] jumps array of AST node types that should match
59
+ #
60
+ # @return [<Type>] <description>
61
+ #
62
+ def node_exists?(ast_node, jumps)
63
+ jumped = walk(ast_node, jumps)
64
+ !!jumped
65
+ end
66
+
67
+ #
68
+ # Recusively `jump`s the AstNode node to find a node that
69
+ # matches all provided jumps. Returns nil if node is
70
+ # not found.
71
+ #
72
+ # In constast `jump` will return any node that matches any
73
+ # of the provided jumps and return the original node if there
74
+ # are no matches.
75
+ #
76
+ # @param [<AstNode>] ast_node A YARD::Parser::Ruby::AstNode
77
+ # @param [[<Symbol>]] jumps through array of AST node types to walk
78
+ #
79
+ # @return [<AstNode, nil>] the requested node or nil
80
+ #
81
+ def walk(ast_node, jumps)
82
+ jumps = Array(jumps)
83
+ node = jumps.inject(ast_node) { |o, j| o.jump(j) }
84
+
85
+ return node if node != ast_node
86
+ end
87
+ end