rfc-reader 1.0.0 → 1.1.1
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 +32 -0
- data/lib/rfc_reader/error_context.rb +102 -0
- data/lib/rfc_reader/library.rb +4 -1
- data/lib/rfc_reader/recent.rb +15 -11
- data/lib/rfc_reader/search.rb +19 -7
- data/lib/rfc_reader/version.rb +1 -1
- data/lib/rfc_reader.rb +8 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc7f0a4142e7e5cb5b1ed4b8a5ecda4728177df15e6f9c8d7e84dcc557b660e4
|
4
|
+
data.tar.gz: 89e7ac7472c84e84d87feed86e66f35f14dbc65a7af39e6a55fe65e158b78e89
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b0588ecb6c225fa8d2ef6c1c27506dda70d59ae2af050797f6ed71536a379423848570022223b04f0911ca3d987431d2b28ab4c019c8c74c31ece1fed3e037a
|
7
|
+
data.tar.gz: 51e9100f05269801e8fcbbe879405181ae0d3910e42593d5a9ad5933d6d8f3f45214cc8a8827d809b75e920029ccfd7584139c707e6a9e3fe0ec12ca750c4f77
|
data/exe/rfc-reader
CHANGED
data/lib/rfc_reader/command.rb
CHANGED
@@ -4,10 +4,17 @@ require "thor"
|
|
4
4
|
|
5
5
|
module RfcReader
|
6
6
|
class Command < Thor
|
7
|
+
SUCCESS = 0
|
8
|
+
private_constant :SUCCESS
|
9
|
+
|
10
|
+
FAILURE = 1
|
11
|
+
private_constant :FAILURE
|
12
|
+
|
7
13
|
# @return [Boolean]
|
8
14
|
def self.exit_on_failure? = true
|
9
15
|
|
10
16
|
# @param command [String, nil]
|
17
|
+
# @return [Integer] exit code
|
11
18
|
def help(command = nil)
|
12
19
|
unless command
|
13
20
|
puts <<~DESCRIPTION
|
@@ -23,6 +30,8 @@ module RfcReader
|
|
23
30
|
end
|
24
31
|
|
25
32
|
super
|
33
|
+
|
34
|
+
SUCCESS
|
26
35
|
end
|
27
36
|
|
28
37
|
desc "search [TERM]", "Search for RFCs by TERM for reading"
|
@@ -33,12 +42,18 @@ module RfcReader
|
|
33
42
|
it will get downloaded so that you can read it in your terminal.
|
34
43
|
LONGDESC
|
35
44
|
# @param term [String]
|
45
|
+
# @return [Integer] exit code
|
36
46
|
def search(term)
|
37
47
|
search_results = Search.search_by(term: term)
|
48
|
+
if search_results.empty?
|
49
|
+
warn "No search results for: #{term}"
|
50
|
+
return FAILURE
|
51
|
+
end
|
38
52
|
title = Terminal.choose("Choose an RFC to read:", search_results.keys)
|
39
53
|
url = search_results.fetch(title)
|
40
54
|
content = Library.download_document(title: title, url: url)
|
41
55
|
Terminal.page(content)
|
56
|
+
SUCCESS
|
42
57
|
end
|
43
58
|
|
44
59
|
desc "recent", "List recent RFC releases for reading"
|
@@ -48,12 +63,18 @@ module RfcReader
|
|
48
63
|
Choose any RFC from the list that seems interesting and
|
49
64
|
it will get downloaded so that you can read it in your terminal.
|
50
65
|
LONGDESC
|
66
|
+
# @return [Integer] exit code
|
51
67
|
def recent
|
52
68
|
recent_results = Recent.list
|
69
|
+
if recent_results.empty?
|
70
|
+
warn "Error: Empty recent RFC list from rfc-editor.org RSS feed"
|
71
|
+
return FAILURE
|
72
|
+
end
|
53
73
|
title = Terminal.choose("Choose an RFC to read:", recent_results.keys)
|
54
74
|
url = recent_results.fetch(title)
|
55
75
|
content = Library.download_document(title: title, url: url)
|
56
76
|
Terminal.page(content)
|
77
|
+
SUCCESS
|
57
78
|
end
|
58
79
|
|
59
80
|
desc "library", "List already downloaded RFCs for reading"
|
@@ -63,18 +84,29 @@ module RfcReader
|
|
63
84
|
Choose any RFC from the list that seems interesting and
|
64
85
|
you can read it offline in your terminal.
|
65
86
|
LONGDESC
|
87
|
+
# @return [Integer] exit code
|
66
88
|
def library
|
67
89
|
rfc_catalog = Library.catalog
|
90
|
+
if rfc_catalog.empty?
|
91
|
+
warn <<~MESSAGE
|
92
|
+
No RFCs are currently saved in the library. Try using the
|
93
|
+
`search` or `recent` commands to download some RFCs first.
|
94
|
+
MESSAGE
|
95
|
+
return FAILURE
|
96
|
+
end
|
68
97
|
all_titles = rfc_catalog.map { _1[:title] }
|
69
98
|
title = Terminal.choose("Choose an RFC to read:", all_titles)
|
70
99
|
rfc = rfc_catalog.find { _1[:title] == title }
|
71
100
|
content = Library.load_document(**rfc)
|
72
101
|
Terminal.page(content)
|
102
|
+
SUCCESS
|
73
103
|
end
|
74
104
|
|
75
105
|
desc "version", "Print the program version"
|
106
|
+
# @return [Integer] exit code
|
76
107
|
def version
|
77
108
|
puts VERSION
|
109
|
+
SUCCESS
|
78
110
|
end
|
79
111
|
end
|
80
112
|
end
|
@@ -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
@@ -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,20 @@ 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
|
+
Nokogiri::XML(xml).xpath("//item").to_h do |item|
|
43
|
+
item_hash = item.elements.to_h do |elem|
|
44
|
+
[elem.name, elem.text.strip]
|
45
|
+
end
|
43
46
|
|
44
|
-
|
45
|
-
|
47
|
+
# The link is to the webpage and not the plaintext document so we must convert it.
|
48
|
+
file_name = File.basename(item_hash.fetch("link"))
|
46
49
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
50
|
+
[
|
51
|
+
item_hash.fetch("title"),
|
52
|
+
"https://www.rfc-editor.org/rfc/#{file_name}.txt",
|
53
|
+
]
|
54
|
+
end
|
51
55
|
end
|
52
56
|
end
|
53
57
|
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/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,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rfc-reader
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Robell
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-10-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -77,6 +77,7 @@ files:
|
|
77
77
|
- exe/rfc-reader
|
78
78
|
- lib/rfc_reader.rb
|
79
79
|
- lib/rfc_reader/command.rb
|
80
|
+
- lib/rfc_reader/error_context.rb
|
80
81
|
- lib/rfc_reader/library.rb
|
81
82
|
- lib/rfc_reader/recent.rb
|
82
83
|
- lib/rfc_reader/search.rb
|