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