rfc-reader 1.0.1 → 1.2.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.
- checksums.yaml +4 -4
- data/exe/rfc-reader +1 -1
- data/lib/rfc_reader/command.rb +15 -12
- data/lib/rfc_reader/error_context.rb +102 -0
- data/lib/rfc_reader/library.rb +4 -1
- data/lib/rfc_reader/recent.rb +14 -14
- data/lib/rfc_reader/search.rb +19 -7
- data/lib/rfc_reader/terminal.rb +63 -5
- data/lib/rfc_reader/version.rb +1 -1
- data/lib/rfc_reader.rb +8 -0
- metadata +18 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d0a782273a9c8f8accd5e24f7b526c7e12e03f870dec176fa7a48fc74650259
|
4
|
+
data.tar.gz: 7c682de619ec580ab71819c50913c609a422696bfb4b64d52b090688fba6773f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a543898b48200f9f4f1e99a592ffdb903e83957bde3df0c8b8c91fee92f2690927309259524c4ee1eb126e33a79aa0b18ef0bcb463c4ef448a611b67321e69ff
|
7
|
+
data.tar.gz: f3e4c4d6f6ba2c4cd4aa5620c7801753c9e1fe4b29adb595014ee27e639e34b7ca37faea7fd79e8c2dc00c7952d60f224d087a85cdbc9e81e1d574c06ccb4c93
|
data/exe/rfc-reader
CHANGED
data/lib/rfc_reader/command.rb
CHANGED
@@ -49,10 +49,11 @@ module RfcReader
|
|
49
49
|
warn "No search results for: #{term}"
|
50
50
|
return FAILURE
|
51
51
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
data/lib/rfc_reader/library.rb
CHANGED
@@ -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 =
|
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)
|
data/lib/rfc_reader/recent.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "net/http"
|
4
|
-
require "
|
4
|
+
require "rss"
|
5
5
|
|
6
6
|
module RfcReader
|
7
7
|
module Recent
|
8
|
-
RECENT_RFCS_RSS_URI = URI("https://www.rfc-editor.org/
|
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
|
-
|
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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
data/lib/rfc_reader/search.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
91
|
+
[title, url]
|
92
|
+
end
|
81
93
|
end
|
82
94
|
end
|
83
95
|
end
|
data/lib/rfc_reader/terminal.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
-
|
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
|
data/lib/rfc_reader/version.rb
CHANGED
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
|
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:
|
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.
|
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: []
|