neverbounce-cli 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +8 -0
  3. data/.gitignore +21 -0
  4. data/.rspec +4 -0
  5. data/.rubocop.yml +1161 -0
  6. data/.travis.yml +27 -0
  7. data/.yardopts +2 -0
  8. data/Gemfile +19 -0
  9. data/Gemfile.lock +60 -0
  10. data/LICENSE +9 -0
  11. data/README.md +138 -0
  12. data/exe/nb-account-info +5 -0
  13. data/exe/nb-jobs-create +5 -0
  14. data/exe/nb-jobs-delete +5 -0
  15. data/exe/nb-jobs-download +5 -0
  16. data/exe/nb-jobs-parse +5 -0
  17. data/exe/nb-jobs-results +5 -0
  18. data/exe/nb-jobs-search +5 -0
  19. data/exe/nb-jobs-start +5 -0
  20. data/exe/nb-jobs-status +5 -0
  21. data/exe/nb-single-check +5 -0
  22. data/lib/never_bounce/cli/feature/basic_initialize.rb +20 -0
  23. data/lib/never_bounce/cli/feature/eigencache.rb +47 -0
  24. data/lib/never_bounce/cli/feature/envars/examples_mapper.rb +38 -0
  25. data/lib/never_bounce/cli/feature/envars/item.rb +41 -0
  26. data/lib/never_bounce/cli/feature/envars.rb +82 -0
  27. data/lib/never_bounce/cli/feature/igetset.rb +41 -0
  28. data/lib/never_bounce/cli/feature/require_attr.rb +25 -0
  29. data/lib/never_bounce/cli/script/account_info.rb +85 -0
  30. data/lib/never_bounce/cli/script/base.rb +101 -0
  31. data/lib/never_bounce/cli/script/error.rb +16 -0
  32. data/lib/never_bounce/cli/script/feature/requires_job_id.rb +28 -0
  33. data/lib/never_bounce/cli/script/feature/uses_pagination.rb +45 -0
  34. data/lib/never_bounce/cli/script/jobs_create/supplied_input_parser.rb +48 -0
  35. data/lib/never_bounce/cli/script/jobs_create.rb +157 -0
  36. data/lib/never_bounce/cli/script/jobs_delete.rb +55 -0
  37. data/lib/never_bounce/cli/script/jobs_download.rb +44 -0
  38. data/lib/never_bounce/cli/script/jobs_parse.rb +73 -0
  39. data/lib/never_bounce/cli/script/jobs_results.rb +105 -0
  40. data/lib/never_bounce/cli/script/jobs_search.rb +135 -0
  41. data/lib/never_bounce/cli/script/jobs_start.rb +73 -0
  42. data/lib/never_bounce/cli/script/jobs_status.rb +96 -0
  43. data/lib/never_bounce/cli/script/manifest.rb +22 -0
  44. data/lib/never_bounce/cli/script/meaningful.rb +233 -0
  45. data/lib/never_bounce/cli/script/request_maker.rb +169 -0
  46. data/lib/never_bounce/cli/script/single_check.rb +146 -0
  47. data/lib/never_bounce/cli/script/table.rb +43 -0
  48. data/lib/never_bounce/cli/user_config/file_content.rb +66 -0
  49. data/lib/never_bounce/cli/user_config.rb +51 -0
  50. data/lib/never_bounce/cli/version.rb +4 -0
  51. data/lib/neverbounce-cli.rb +3 -0
  52. data/neverbounce-cli.gemspec +24 -0
  53. data/spec/lib/never_bounce/cli/feature/basic_initialize_spec.rb +26 -0
  54. data/spec/lib/never_bounce/cli/feature/eigencache_spec.rb +28 -0
  55. data/spec/lib/never_bounce/cli/feature/envars/examples_mapper_spec.rb +26 -0
  56. data/spec/lib/never_bounce/cli/feature/envars/item_spec.rb +9 -0
  57. data/spec/lib/never_bounce/cli/feature/envars_spec.rb +55 -0
  58. data/spec/lib/never_bounce/cli/feature/igetset_spec.rb +45 -0
  59. data/spec/lib/never_bounce/cli/feature/require_attr_spec.rb +25 -0
  60. data/spec/lib/never_bounce/cli/script/account_info_spec.rb +65 -0
  61. data/spec/lib/never_bounce/cli/script/base_spec.rb +56 -0
  62. data/spec/lib/never_bounce/cli/script/feature/requires_job_id_spec.rb +17 -0
  63. data/spec/lib/never_bounce/cli/script/feature/uses_pagination_spec.rb +21 -0
  64. data/spec/lib/never_bounce/cli/script/jobs_create/supplied_input_parser_spec.rb +35 -0
  65. data/spec/lib/never_bounce/cli/script/jobs_create_spec.rb +118 -0
  66. data/spec/lib/never_bounce/cli/script/jobs_delete_spec.rb +33 -0
  67. data/spec/lib/never_bounce/cli/script/jobs_download_spec.rb +37 -0
  68. data/spec/lib/never_bounce/cli/script/jobs_parse_spec.rb +45 -0
  69. data/spec/lib/never_bounce/cli/script/jobs_results_spec.rb +40 -0
  70. data/spec/lib/never_bounce/cli/script/jobs_search_spec.rb +59 -0
  71. data/spec/lib/never_bounce/cli/script/jobs_start_spec.rb +45 -0
  72. data/spec/lib/never_bounce/cli/script/jobs_status_spec.rb +37 -0
  73. data/spec/lib/never_bounce/cli/script/manifest_spec.rb +6 -0
  74. data/spec/lib/never_bounce/cli/script/meaningful_spec.rb +93 -0
  75. data/spec/lib/never_bounce/cli/script/request_maker_spec.rb +67 -0
  76. data/spec/lib/never_bounce/cli/script/single_check_spec.rb +94 -0
  77. data/spec/lib/never_bounce/cli/script/spec_helper.rb +59 -0
  78. data/spec/lib/never_bounce/cli/script/table_spec.rb +6 -0
  79. data/spec/lib/never_bounce/cli/user_config/file_content_spec.rb +43 -0
  80. data/spec/lib/never_bounce/cli/user_config_spec.rb +12 -0
  81. data/spec/lib/never_bounce/cli_spec.rb +6 -0
  82. data/spec/spec_helper.rb +51 -0
  83. metadata +193 -0
@@ -0,0 +1,169 @@
1
+
2
+ require "never_bounce/cli/script/table"
3
+ require "never_bounce/cli/user_config"
4
+
5
+ require_relative "meaningful"
6
+
7
+ module NeverBounce; module CLI; module Script
8
+ # An API request maker script base class.
9
+ class RequestMaker < Meaningful
10
+ attr_writer :api_key, :request, :request_curl, :response, :server_raw, :session, :user_config
11
+
12
+ envar "API_KEY*", "API key", ["2ed45186c72f9319dc64338cdf16ab76b44cf3d1"]
13
+ envar "API_URL", "Custom API URL", ["https://staging-api.isp.com/v5"]
14
+ envar "CURL", "Print cURL request and exit", ["y", default: "N"]
15
+ envar "RAW", "Print raw response body", ["y", default: "N"]
16
+
17
+ # Shared envar defaults.
18
+ SHARED_ENVARS = {
19
+ "AUTO_START" => ["Start processing the job immediately after parsing", ["y", "n"]],
20
+ "RUN_SAMPLE" => ["Run this job as a sample", ["y", "n"]],
21
+ }
22
+
23
+ # @!attribute api_key
24
+ # @return [String]
25
+ def api_key
26
+ @api_key ||= env["API_KEY"] || user_config.api_key or raise UsageError, "API key not given, use `API_KEY=`"
27
+ end
28
+
29
+ # @!attribute api_url
30
+ # @return [String]
31
+ def api_url
32
+ @api_url ||= igetset(:api_url) do
33
+ env["API_URL"] || user_config.api_url
34
+ end
35
+ end
36
+
37
+ # An instance of <tt>API::Request::Base</tt> successor.
38
+ # @abstract
39
+ # @!attribute request
40
+ # @return [Object]
41
+ def request
42
+ @request or raise NotImplementedError, "Redefine `request` in your class: #{self.class}"
43
+ end
44
+
45
+ # Request's cURL representation. Default is <tt>request.to_curl</tt>
46
+ # @!attribute request_curl
47
+ # @return [Array]
48
+ def request_curl
49
+ require_attr(:request).to_curl
50
+ end
51
+
52
+ # An <tt>API::Response::Base</tt> successor instance.
53
+ # @!attribute response
54
+ # @return [Object]
55
+ def response
56
+ @response ||= require_attr(:session).response
57
+ end
58
+
59
+ # Raw server response text. Default is <tt>session.server_raw</tt>.
60
+ # This attribute is used by <tt>RAW=y</tt> mode only.
61
+ # @!attribute server_raw
62
+ # @return [String]
63
+ def server_raw
64
+ @server_raw ||= require_attr(:session).server_raw
65
+ end
66
+
67
+ # An instance of <tt>API::Session</tt> built around {#request}.
68
+ # @!attribute session
69
+ # @return [Object]
70
+ def session
71
+ @session ||= API::Session.new(request: require_attr(:request))
72
+ end
73
+
74
+ # @!attribute user_config
75
+ # @return [UserConfig]
76
+ def user_config
77
+ @user_config ||= UserConfig.new
78
+ end
79
+
80
+ #--------------------------------------- Misc
81
+
82
+ # Extract response's value according to a heading spec.
83
+ #
84
+ # get_table_value(reasponse, ["ID", :id, :right])
85
+ def get_table_value(r, hspec)
86
+ if (m = hspec[1]).is_a? Proc
87
+ m.(r)
88
+ else
89
+ r.public_send(m)
90
+ end
91
+ end
92
+ private :get_table_value
93
+
94
+ # "Inspect or nil" -- format a scalar for table-friendly output.
95
+ #
96
+ # inil(5) # => "5"
97
+ # inil(nil) # => "-"
98
+ #
99
+ # @return [String]
100
+ def inil(v)
101
+ v.nil?? "-" : v.inspect
102
+ end
103
+ private :inil
104
+
105
+ # Print request as a ready-to-run cURL command. Return 0.
106
+ def print_curl_request
107
+ stdout.puts "curl #{request_curl.map(&:shellescape).join(' ')}"
108
+ 0
109
+ end
110
+
111
+ # Print error response as a standard table. Return 1.
112
+ #
113
+ # return print_error_response if response_error?
114
+ def print_error_response
115
+ "ErrorResponse".tap do |label|
116
+ headings = [
117
+ ["Status", :status, :center],
118
+ ["Message", :message],
119
+ ["ExecTime", :execution_time, :right],
120
+ ]
121
+
122
+ table = Table.new(
123
+ headings: headings.map { |ar| ar[0] },
124
+ rows: [headings.map { |ar| get_table_value(response, ar) }],
125
+ ).align!(headings)
126
+
127
+ stdout.puts "\n#{label}:"
128
+ stdout.puts table
129
+ end
130
+
131
+ 1
132
+ end
133
+
134
+ # Print raw response. Return 0.
135
+ #
136
+ # return print_raw_response if env_truthy? "RAW"
137
+ def print_server_raw
138
+ stdout.puts server_raw
139
+ 0
140
+ end
141
+
142
+ #--------------------------------------- Main
143
+
144
+ def slim_main1
145
+ # Perform common boilerplate actions.
146
+ return print_curl_request if env_truthy? "CURL"
147
+
148
+ # Any of these, unless during tests, touch `response`, which triggers the actual request.
149
+ return print_server_raw if env_truthy? "RAW"
150
+ return print_error_response if response.error?
151
+
152
+ call_slim_main(0)
153
+ end
154
+ end
155
+ end; end; end
156
+
157
+ #
158
+ # Implementation notes:
159
+ #
160
+ # * See `def request` for a cool testing hack: `@request || raise`.
161
+ # It makes it possible to test abstract's class functionality without having to create successors
162
+ # just to stub an attribute.
163
+ # * Column output order in all scripts is logical+alphabetical.
164
+ # Most meaningful attributes are output first, the rest are loosely grouped based on decreasing
165
+ # importance.
166
+ # * We deliberately don't use `OptionParser` in final scripts, using "meta-documented" env variables
167
+ # instead. See `envar` and stuff.
168
+ # * `manifest` is an instance method to simplify its usage in output code. `self.class.manifest` is
169
+ # clunky and thus less smart.
@@ -0,0 +1,146 @@
1
+
2
+ require "neverbounce"
3
+
4
+ require_relative "request_maker"
5
+
6
+ module NeverBounce; module CLI; module Script
7
+ class SingleCheck < RequestMaker
8
+ attr_writer :address_info, :email, :credits_info, :timeout
9
+
10
+ envar "ADDRESS_INFO", "Request additional address info", ["y", "n"]
11
+ envar "EMAIL*", "E-mail to check", ["alice@isp.com", "bob.smith+1@domain.com"]
12
+ envar "CREDITS_INFO", "Request additional credits info", ["y", "n"]
13
+ envar "TIMEOUT", "Timeout in seconds to verify the address", ["5"]
14
+
15
+ # @return [true]
16
+ # @return [false]
17
+ # @return [nil]
18
+ def address_info
19
+ igetset(:address_info) do
20
+ if env.has_key?(k = "ADDRESS_INFO")
21
+ env_truthy?(k)
22
+ end
23
+ end
24
+ end
25
+
26
+ # @return [true]
27
+ # @return [false]
28
+ # @return [nil]
29
+ def credits_info
30
+ igetset(:credits_info) do
31
+ if env.has_key?(k = "CREDITS_INFO")
32
+ env_truthy?(k)
33
+ end
34
+ end
35
+ end
36
+
37
+ def email
38
+ @email ||= env[k = "EMAIL"] or raise UsageError, "E-mail address not given, use `#{k}=`"
39
+ end
40
+
41
+ # An <tt>API::Request::SingleCheck</tt>.
42
+ # @!attribute request
43
+ # @return [Object]
44
+ def request
45
+ @request ||= API::Request::SingleCheck.new({
46
+ address_info: address_info,
47
+ api_key: api_key,
48
+ credits_info: credits_info,
49
+ email: email,
50
+ timeout: timeout,
51
+ })
52
+ end
53
+
54
+ def timeout
55
+ igetset(:timeout) do
56
+ if (v = env["TIMEOUT"])
57
+ begin
58
+ Integer(v)
59
+ rescue ArgumentError => e
60
+ raise UsageError, e.message
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ #--------------------------------------- Manifest
67
+
68
+ # @!attribute manifest
69
+ # @return [Manifest]
70
+ def manifest
71
+ @manifest ||= Manifest.new(
72
+ name: "nb-single-check",
73
+ function: "Check a single e-mail",
74
+ cmdline: "[options] [VAR1=value] [VAR2=value] ...",
75
+ )
76
+ end
77
+
78
+ #--------------------------------------- Main
79
+
80
+ # @return [Integer]
81
+ def slim_main
82
+ "Response".tap do |label|
83
+ headings = [
84
+ ["Result", :result, :center],
85
+ [
86
+ "Flags",
87
+ ->(r) { r.flags.sort.join("\n") },
88
+ ],
89
+ ["SuggCorr", :suggested_correction],
90
+
91
+ ["ExecTime", :execution_time, :right],
92
+ ]
93
+
94
+ table = Table.new(
95
+ headings: headings.map { |ar| ar[0] },
96
+ rows: [headings.map { |ar| get_table_value(response, ar) }],
97
+ ).align!(headings)
98
+
99
+ stdout.puts "\n#{label}:"
100
+ stdout.puts table
101
+ end
102
+
103
+ response.address_info? and "AddressInfo".tap do |label|
104
+ headings = [
105
+ ["Addr", :addr],
106
+ ["Alias", :alias],
107
+ ["Domain", :domain],
108
+ ["FQDN", :fqdn],
109
+ ["Host", :host],
110
+ ["NormEmail", :normalized_email],
111
+ ["OrigEmail", :original_email],
112
+ ["Subdomain", :subdomain],
113
+ ["TLD", :tld],
114
+ ]
115
+
116
+ table = Table.new(
117
+ headings: headings.map { |ar| ar[0] },
118
+ rows: [headings.map { |ar| get_table_value(response.address_info, ar) }],
119
+ ).align!(headings)
120
+
121
+ stdout.puts "\n#{label}:"
122
+ stdout.puts table
123
+ end # response.address_info?
124
+
125
+ response.credits_info? and "CreditsInfo".tap do |label|
126
+ headings = [
127
+ ["FreeRmn", :free_credits_remaining, :right],
128
+ ["FreeUsed", :free_credits_used, :right],
129
+ (["MonthlyUsage", :monthly_api_usage, :right] if response.credits_info.monthly?),
130
+ (["PaidRmn", :paid_credits_remaining, :right] if response.credits_info.paid?),
131
+ (["PaidUsed", :paid_credits_used, :right] if response.credits_info.paid?),
132
+ ].compact
133
+
134
+ table = Table.new(
135
+ headings: headings.map { |ar| ar[0] },
136
+ rows: [headings.map { |ar| get_table_value(response.credits_info, ar) }],
137
+ ).align!(headings)
138
+
139
+ stdout.puts "\n#{label}:"
140
+ stdout.puts table
141
+ end # response.credits_info?
142
+
143
+ 0
144
+ end
145
+ end
146
+ end; end; end
@@ -0,0 +1,43 @@
1
+
2
+ require "terminal-table"
3
+
4
+ module NeverBounce; module CLI; module Script
5
+ # Our custom table class.
6
+ class Table < Terminal::Table
7
+ # Align table rows according to headings spec.
8
+ #
9
+ # headings = [
10
+ # ["Status", :status],
11
+ # ["Completed", :completed, :right],
12
+ # ["Processing", :processing, :right],
13
+ # ]
14
+ #
15
+ # table = Table.new(headings: ..., rows: ...).align!(headings)
16
+ # puts table
17
+ #
18
+ # NOTE: Invoke <b>after</b> adding row data.
19
+ #
20
+ # @return [self]
21
+ def align!(headings)
22
+ headings.each_with_index do |ar, i|
23
+ if (v = ar[2])
24
+ align_column(i, v)
25
+ end
26
+ end
27
+
28
+ self
29
+ end
30
+
31
+ # Center-align headings by default.
32
+ # @return [void]
33
+ def headings=(ar)
34
+ super(ar.map do |item|
35
+ if item.is_a? String
36
+ {value: item, alignment: :center}
37
+ else
38
+ item
39
+ end
40
+ end)
41
+ end
42
+ end
43
+ end; end; end
@@ -0,0 +1,66 @@
1
+
2
+ require "yaml"
3
+
4
+ require "never_bounce/cli/feature/basic_initialize"
5
+
6
+ require_relative "../user_config"
7
+
8
+ module NeverBounce; module CLI; class UserConfig
9
+ # User's configuration file content.
10
+ # @see #filename
11
+ # @see UserConfig
12
+ # @see CLI::Feature::BasicInitialize
13
+ class FileContent
14
+ CLI::Feature::BasicInitialize.load(self)
15
+
16
+ attr_writer :body, :body_hash, :env, :filename
17
+
18
+ # YAML configuration, source text.
19
+ # @!attribute body
20
+ # @return [String]
21
+ def body
22
+ @body ||= begin
23
+ File.read(filename)
24
+ rescue Errno::ENOENT # Missing file is okay, let other exceptions manifest.
25
+ ""
26
+ end
27
+ end
28
+
29
+ # YAML configuration, parsed.
30
+ # @!attribute body_hash
31
+ # @return [Hash]
32
+ def body_hash
33
+ @body_hash ||= body.to_s.empty?? {} : YAML.load(body)
34
+ end
35
+
36
+ # A copy of environment for read purposes. Default is <tt>ENV.to_h</tt>.
37
+ # @return [Hash]
38
+ def env
39
+ @env ||= ENV.to_h
40
+ end
41
+
42
+ # Configuration filename. Default is <tt>$HOME/.neverbounce.yml</tt>.
43
+ # @return [String]
44
+ def filename
45
+ @filename ||= File.join(env["HOME"], ".neverbounce.yml")
46
+ end
47
+
48
+ #---------------------------------------
49
+
50
+ # Fetch a value.
51
+ #
52
+ # config_file["api_key"]
53
+ # config_file[:api_key] # Same as above.
54
+ def [](k)
55
+ body_hash[k.to_s]
56
+ end
57
+
58
+ # <tt>true</tt> if key is set.
59
+ #
60
+ # has_key?("api_key")
61
+ # has_key?(:api_key) # Identical to the previous one.
62
+ def has_key?(k)
63
+ body_hash.has_key? k.to_s
64
+ end
65
+ end
66
+ end; end; end
@@ -0,0 +1,51 @@
1
+
2
+ require "never_bounce/cli/feature/eigencache"
3
+
4
+ module NeverBounce; module CLI
5
+ # User's configuration values.
6
+ # @see UserConfig::FileContent
7
+ class UserConfig
8
+ require_relative "user_config/file_content"
9
+
10
+ CLI::Feature::BasicInitialize.load(self)
11
+ CLI::Feature::Eigencache.load(self)
12
+
13
+ attr_writer :api_key, :api_url
14
+
15
+ # API key.
16
+ # @return [String]
17
+ def api_key
18
+ @api_key ||= fc[:api_key]
19
+ end
20
+
21
+ # API URL.
22
+ # @return [String]
23
+ def api_url
24
+ @api_url ||= fc[:api_url]
25
+ end
26
+
27
+ #---------------------------------------
28
+
29
+ # @!attribute fc
30
+ # @return [FileContent]
31
+ def fc
32
+ _cache[:fc] ||= FileContent.new
33
+ end
34
+
35
+ def fc=(obj)
36
+ _cache[:fc] = obj
37
+ end
38
+
39
+ # "Touch" all attributes by loading them.
40
+ # @return [self]
41
+ def touch
42
+ api_key; api_url
43
+ self
44
+ end
45
+ end
46
+ end; end
47
+
48
+ #
49
+ # Implementation notes:
50
+ #
51
+ # * `api_url` has got no default here, it isn't a config responsibility.