rfc-reader 1.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f626967d7919c641d14227967a9e863a2c4eaedfd5f97dc5ecde994349725cd5
4
- data.tar.gz: 303d9bf3449ac2122f27f36f3053dfdbb7350b1f8900662517055aadfe8a6c42
3
+ metadata.gz: 9d0a782273a9c8f8accd5e24f7b526c7e12e03f870dec176fa7a48fc74650259
4
+ data.tar.gz: 7c682de619ec580ab71819c50913c609a422696bfb4b64d52b090688fba6773f
5
5
  SHA512:
6
- metadata.gz: 1c29a80e008410ed0924921850c70ad84b8a8bf99e341725a6164757204db8469e7f4eab9c725e04557098251e9d22c23861eb1fbe9a0a9c1cd75603392d9aff
7
- data.tar.gz: 3351a9b4447a38eabf3eb9ba5f0fd50415e9502af6db5fadd236ee1ab848ff57bde6b959b0a6ceaafb337387310f7ca656b9214b7ec063879df51daa275415f7
6
+ metadata.gz: a543898b48200f9f4f1e99a592ffdb903e83957bde3df0c8b8c91fee92f2690927309259524c4ee1eb126e33a79aa0b18ef0bcb463c4ef448a611b67321e69ff
7
+ data.tar.gz: f3e4c4d6f6ba2c4cd4aa5620c7801753c9e1fe4b29adb595014ee27e639e34b7ca37faea7fd79e8c2dc00c7952d60f224d087a85cdbc9e81e1d574c06ccb4c93
data/exe/rfc-reader CHANGED
@@ -4,4 +4,4 @@
4
4
  $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
5
  require "rfc_reader"
6
6
 
7
- exit RfcReader::Command.start
7
+ exit RfcReader.start
@@ -49,10 +49,11 @@ module RfcReader
49
49
  warn "No search results for: #{term}"
50
50
  return FAILURE
51
51
  end
52
- title = Terminal.choose("Choose an RFC to read:", search_results.keys)
53
- url = search_results.fetch(title)
54
- content = Library.download_document(title: title, url: url)
55
- Terminal.page(content)
52
+ Terminal.choose("Choose an RFC to read:", search_results.keys) do |title|
53
+ url = search_results.fetch(title)
54
+ content = Library.download_document(title: title, url: url)
55
+ Terminal.page(content)
56
+ end
56
57
  SUCCESS
57
58
  end
58
59
 
@@ -70,10 +71,11 @@ module RfcReader
70
71
  warn "Error: Empty recent RFC list from rfc-editor.org RSS feed"
71
72
  return FAILURE
72
73
  end
73
- title = Terminal.choose("Choose an RFC to read:", recent_results.keys)
74
- url = recent_results.fetch(title)
75
- content = Library.download_document(title: title, url: url)
76
- Terminal.page(content)
74
+ Terminal.choose("Choose an RFC to read:", recent_results.keys) do |title|
75
+ url = recent_results.fetch(title)
76
+ content = Library.download_document(title: title, url: url)
77
+ Terminal.page(content)
78
+ end
77
79
  SUCCESS
78
80
  end
79
81
 
@@ -95,10 +97,11 @@ module RfcReader
95
97
  return FAILURE
96
98
  end
97
99
  all_titles = rfc_catalog.map { _1[:title] }
98
- title = Terminal.choose("Choose an RFC to read:", all_titles)
99
- rfc = rfc_catalog.find { _1[:title] == title }
100
- content = Library.load_document(**rfc)
101
- Terminal.page(content)
100
+ Terminal.choose("Choose an RFC to read:", all_titles) do |title|
101
+ rfc = rfc_catalog.find { _1[:title] == title }
102
+ content = Library.load_document(**rfc)
103
+ Terminal.page(content)
104
+ end
102
105
  SUCCESS
103
106
  end
104
107
 
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module RfcReader
6
+ module ErrorContext
7
+ class ContextError < StandardError
8
+ extend Forwardable
9
+
10
+ attr_reader :context
11
+
12
+ def_delegators :@cause, :message, :to_s, :backtrace, :backtrace_locations
13
+
14
+ # @param cause [StandardError]
15
+ # @param context [String] (Optional)
16
+ def initialize(cause:, context: nil)
17
+ @cause = cause
18
+ @context = context || default_context
19
+ super
20
+ end
21
+
22
+ def short_message
23
+ "Error: #{context}"
24
+ end
25
+
26
+ def full_message
27
+ <<~MESSAGE
28
+ Context:
29
+ #{indent(@context)}
30
+ Error:
31
+ #{@cause.class}
32
+ Message:
33
+ #{indent(message)}
34
+ Backtrace:
35
+ #{indent(backtrace)}
36
+ MESSAGE
37
+ end
38
+
39
+ private
40
+
41
+ def default_context
42
+ "#{@cause.class}: #{message.lines.first}"
43
+ end
44
+
45
+ def indent(string_or_array)
46
+ strings = case string_or_array
47
+ when Array then string_or_array
48
+ when String then string_or_array.lines
49
+ else raise ArgumentError, "Expected string or array instead of #{string_or_array.class}"
50
+ end
51
+
52
+ strings
53
+ .map { _1.prepend(" ") }
54
+ .join("\n")
55
+ end
56
+ end
57
+
58
+ # Yields an error context. Any `StandardError` that gets raised in this block
59
+ # gets wrapped by `ContextError` automatically and re-raised.
60
+ #
61
+ # If no error has been raised, it returns the result of the block.
62
+ #
63
+ # @param context [String]
64
+ # @return yielded block value
65
+ def self.wrap(context)
66
+ yield
67
+ rescue StandardError => e
68
+ raise ContextError.new(cause: e, context: context)
69
+ end
70
+
71
+ # Yields a handler context where any `StandardError` is caught and the error message is
72
+ # printed to stderr along with the context. It prints only a short message by default
73
+ # and prints the full error message if the `DEBUG` error message is set. It then exits
74
+ # with a non-zero error code.
75
+ #
76
+ # If no error has been raised, it returns the result of the block as an integer.
77
+ # If the result of block cannot be turned into an integer, it returns zero.
78
+ #
79
+ # @return [Integer] exit code
80
+ def self.handler
81
+ error = nil
82
+
83
+ begin
84
+ result = yield
85
+ return result.respond_to?(:to_i) ? result.to_i : 0
86
+ rescue ContextError => e
87
+ error = e
88
+ rescue StandardError => e
89
+ error = ContextError.new(cause: e)
90
+ end
91
+
92
+ if ENV["DEBUG"]
93
+ warn error.full_message
94
+ else
95
+ warn error.short_message
96
+ warn "Note: Set the `DEBUG` environment variable to see the full error context"
97
+ end
98
+
99
+ 1
100
+ end
101
+ end
102
+ end
@@ -16,7 +16,10 @@ module RfcReader
16
16
  file_name = File.basename(url)
17
17
  file_path = File.join(library_cache_dir, file_name)
18
18
 
19
- content = Net::HTTP.get(URI(url))
19
+ content = ErrorContext.wrap("Downloading RFC document") do
20
+ Net::HTTP.get(URI(url))
21
+ end
22
+
20
23
  FileUtils.mkdir_p(library_cache_dir)
21
24
  File.write(file_path, content)
22
25
  add_to_catalog(title: title, url: url, path: file_path)
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
- require "nokogiri"
4
+ require "rss"
5
5
 
6
6
  module RfcReader
7
7
  module Recent
8
- RECENT_RFCS_RSS_URI = URI("https://www.rfc-editor.org/rfcrss.xml").freeze
8
+ RECENT_RFCS_RSS_URI = URI("https://www.rfc-editor.org/rfcatom.xml").freeze
9
9
  private_constant :RECENT_RFCS_RSS_URI
10
10
 
11
11
  # @return [Hash<String, String>] from RFC title to text file url
@@ -16,7 +16,9 @@ module RfcReader
16
16
 
17
17
  # @return [String] the raw XML from the recent RFCs RSS feed
18
18
  def self.fetch
19
- Net::HTTP.get(RECENT_RFCS_RSS_URI)
19
+ ErrorContext.wrap("Fetching the recent RFCs list") do
20
+ Net::HTTP.get(RECENT_RFCS_RSS_URI)
21
+ end
20
22
  end
21
23
 
22
24
  # Example: XML fragment we're trying to parse title and link data from.
@@ -36,18 +38,16 @@ module RfcReader
36
38
  # @param xml [String] the XML of the recent RFCs RSS endpoint
37
39
  # @return [Hash<String, String>] from RFC title to text file url
38
40
  def self.parse(xml)
39
- Nokogiri::XML(xml).xpath("//item").to_h do |item|
40
- item_hash = item.elements.to_h do |elem|
41
- [elem.name, elem.text.strip]
42
- end
41
+ ErrorContext.wrap("Parsing the recent RFCs list") do
42
+ RSS::Parser.parse(xml).items.to_h do |item|
43
+ # The link is to the webpage and not the plaintext document so we must convert it.
44
+ file_name = File.basename(item.link.href)
43
45
 
44
- # The link is to the webpage and not the plaintext document so we must convert it.
45
- file_name = File.basename(item_hash.fetch("link"))
46
-
47
- [
48
- item_hash.fetch("title"),
49
- "https://www.rfc-editor.org/rfc/#{file_name}.txt",
50
- ]
46
+ [
47
+ item.title.content,
48
+ "https://www.rfc-editor.org/rfc/#{file_name}.txt",
49
+ ]
50
+ end
51
51
  end
52
52
  end
53
53
  end
@@ -18,7 +18,9 @@ module RfcReader
18
18
  # @param term [String]
19
19
  # @return [String] the raw HTML of the search results for the given term
20
20
  def self.fetch_by(term:)
21
- Net::HTTP.post_form(RFC_SEARCH_URI, { combo_box: term }).body
21
+ ErrorContext.wrap("Fetching RFC search results") do
22
+ Net::HTTP.post_form(RFC_SEARCH_URI, { combo_box: term }).body
23
+ end
22
24
  end
23
25
 
24
26
  # Example: HTML fragment we're trying to parse title and link info from.
@@ -71,13 +73,23 @@ module RfcReader
71
73
  # @param html [String] the HTML of the search results
72
74
  # @return [Hash<String, String>] from RFC title to text file url
73
75
  def self.parse(html)
74
- # NOTE: The first element in the table is just some general search information. See example HTML above.
75
- Nokogiri::HTML(html).xpath("//div[@class='scrolltable']//table[@class='gridtable']//tr").drop(1).to_h do |tr_node|
76
- td_nodes = tr_node.elements
77
- title = td_nodes[2].text.strip
78
- url = td_nodes[1].elements.map { _1.attribute("href").text.strip }.find { _1.end_with?(".txt") }
76
+ ErrorContext.wrap("Parsing RFC search results") do
77
+ # NOTE: The first element in the table is just some general search information. See example HTML above.
78
+ Nokogiri::HTML(html)
79
+ .xpath("//div[@class='scrolltable']//table[@class='gridtable']//tr")
80
+ .drop(1)
81
+ .to_h do |tr_node|
82
+ td_nodes = tr_node.elements
83
+ title = td_nodes[2]
84
+ .text
85
+ .strip
86
+ url = td_nodes[1]
87
+ .elements
88
+ .map { _1.attribute("href").text.strip }
89
+ .find { _1.end_with?(".txt") }
79
90
 
80
- [title, url]
91
+ [title, url]
92
+ end
81
93
  end
82
94
  end
83
95
  end
@@ -10,17 +10,75 @@ module RfcReader
10
10
 
11
11
  # @param prompt [String]
12
12
  # @param choices [Array<String>] where all choices are unique
13
- def self.choose(prompt, choices)
14
- require "tty-prompt"
15
- TTY::Prompt
16
- .new(
13
+ # @yield [String] yields until the user exits the prompt gracefully
14
+ def self.choose(message, choices)
15
+ while (choice = selector.select(message, choices))
16
+ yield choice
17
+ end
18
+ end
19
+
20
+ # @return [Selector]
21
+ def self.selector
22
+ @selector ||= Selector.new
23
+ end
24
+ private_class_method :selector
25
+
26
+ # Wrapper around `TTY::Prompt` that adds Vim keybindings and
27
+ # the ability to gracefully exit the prompt.
28
+ class Selector
29
+ def initialize
30
+ require "tty-prompt"
31
+ @prompt = TTY::Prompt.new(
17
32
  quiet: true,
18
33
  track_history: false,
19
34
  interrupt: :exit,
20
35
  symbols: { marker: ">" },
21
36
  enable_color: !ENV["NO_COLOR"]
22
37
  )
23
- .select(prompt, choices)
38
+
39
+ # Indicate user intention to exit
40
+ @exit = false
41
+
42
+ # vim keybindings
43
+ @prompt.on(:keypress) do |event|
44
+ case event.value
45
+ when "j" # Move down
46
+ @prompt.trigger(:keydown)
47
+ when "k" # Move up
48
+ @prompt.trigger(:keyup)
49
+ when "h" # Move left
50
+ @prompt.trigger(:keyleft)
51
+ when "l" # Move right
52
+ @prompt.trigger(:keyright)
53
+ when "q" # Exit
54
+ @exit = true
55
+ @prompt.trigger(:keyenter)
56
+ end
57
+ end
58
+
59
+ # Exit on escape
60
+ @prompt.on(:keyescape) do
61
+ @exit = true
62
+ @prompt.trigger(:keyenter)
63
+ end
64
+ end
65
+
66
+ # @param prompt [String]
67
+ # @param choices [Array<String>] where all choices are unique
68
+ # @return [String|nil]
69
+ def select(message, choices)
70
+ choice = @prompt.select(
71
+ message,
72
+ choices,
73
+ per_page: 15,
74
+ help: "(Press Enter to select and Escape to leave)",
75
+ show_help: :always
76
+ )
77
+ choice unless @exit
78
+ ensure
79
+ @exit = false
80
+ end
24
81
  end
82
+ private_constant :Selector
25
83
  end
26
84
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RfcReader
4
- VERSION = "1.0.1"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/rfc_reader.rb CHANGED
@@ -8,4 +8,12 @@ module RfcReader
8
8
  autoload :Search, "rfc_reader/search"
9
9
  autoload :Terminal, "rfc_reader/terminal"
10
10
  autoload :Library, "rfc_reader/library"
11
+ autoload :ErrorContext, "rfc_reader/error_context"
12
+
13
+ # @return [Integer] exit code
14
+ def self.start
15
+ ErrorContext.handler do
16
+ Command.start
17
+ end
18
+ end
11
19
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfc-reader
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Robell
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-09-28 00:00:00.000000000 Z
10
+ date: 2025-01-20 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: nokogiri
@@ -24,6 +23,20 @@ dependencies:
24
23
  - - "~>"
25
24
  - !ruby/object:Gem::Version
26
25
  version: '1.16'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rss
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.3'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.3'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: thor
29
42
  requirement: !ruby/object:Gem::Requirement
@@ -66,7 +79,6 @@ dependencies:
66
79
  - - "~>"
67
80
  - !ruby/object:Gem::Version
68
81
  version: '0.23'
69
- description:
70
82
  email:
71
83
  - apainintheneck@gmail.com
72
84
  executables:
@@ -77,6 +89,7 @@ files:
77
89
  - exe/rfc-reader
78
90
  - lib/rfc_reader.rb
79
91
  - lib/rfc_reader/command.rb
92
+ - lib/rfc_reader/error_context.rb
80
93
  - lib/rfc_reader/library.rb
81
94
  - lib/rfc_reader/recent.rb
82
95
  - lib/rfc_reader/search.rb
@@ -90,7 +103,6 @@ metadata:
90
103
  source_code_uri: https://github.com/apainintheneck/rfc-reader/
91
104
  changelog_uri: https://github.com/apainintheneck/rfc-reader/blob/main/CHANELOG.md
92
105
  rubygems_mfa_required: 'true'
93
- post_install_message:
94
106
  rdoc_options: []
95
107
  require_paths:
96
108
  - lib
@@ -105,8 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
105
117
  - !ruby/object:Gem::Version
106
118
  version: '0'
107
119
  requirements: []
108
- rubygems_version: 3.5.6
109
- signing_key:
120
+ rubygems_version: 3.6.3
110
121
  specification_version: 4
111
122
  summary: Search for and read RFCs at the command line.
112
123
  test_files: []