neverbounce-api 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) 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 +17 -0
  9. data/Gemfile.lock +48 -0
  10. data/LICENSE +9 -0
  11. data/README.md +235 -0
  12. data/lib/never_bounce/api/client.rb +259 -0
  13. data/lib/never_bounce/api/error.rb +21 -0
  14. data/lib/never_bounce/api/feature/basic_initialize.rb +20 -0
  15. data/lib/never_bounce/api/feature/eigencache.rb +48 -0
  16. data/lib/never_bounce/api/feature/igetset.rb +41 -0
  17. data/lib/never_bounce/api/feature/oattrs.rb +100 -0
  18. data/lib/never_bounce/api/feature/require_attr.rb +27 -0
  19. data/lib/never_bounce/api/request/account_info.rb +29 -0
  20. data/lib/never_bounce/api/request/base.rb +102 -0
  21. data/lib/never_bounce/api/request/jobs_create.rb +90 -0
  22. data/lib/never_bounce/api/request/jobs_delete.rb +34 -0
  23. data/lib/never_bounce/api/request/jobs_download.rb +34 -0
  24. data/lib/never_bounce/api/request/jobs_parse.rb +55 -0
  25. data/lib/never_bounce/api/request/jobs_results.rb +53 -0
  26. data/lib/never_bounce/api/request/jobs_search.rb +57 -0
  27. data/lib/never_bounce/api/request/jobs_start.rb +47 -0
  28. data/lib/never_bounce/api/request/jobs_status.rb +37 -0
  29. data/lib/never_bounce/api/request/single_check.rb +67 -0
  30. data/lib/never_bounce/api/response/account_info.rb +39 -0
  31. data/lib/never_bounce/api/response/account_info/job_counts.rb +26 -0
  32. data/lib/never_bounce/api/response/address_info.rb +43 -0
  33. data/lib/never_bounce/api/response/base.rb +15 -0
  34. data/lib/never_bounce/api/response/container.rb +126 -0
  35. data/lib/never_bounce/api/response/credits_info/base.rb +30 -0
  36. data/lib/never_bounce/api/response/credits_info/monthly.rb +16 -0
  37. data/lib/never_bounce/api/response/credits_info/paid.rb +20 -0
  38. data/lib/never_bounce/api/response/error_message.rb +17 -0
  39. data/lib/never_bounce/api/response/feature/job_status_fields.rb +68 -0
  40. data/lib/never_bounce/api/response/feature/job_status_fields/total.rb +46 -0
  41. data/lib/never_bounce/api/response/jobs_create.rb +10 -0
  42. data/lib/never_bounce/api/response/jobs_delete.rb +8 -0
  43. data/lib/never_bounce/api/response/jobs_download.rb +19 -0
  44. data/lib/never_bounce/api/response/jobs_parse.rb +10 -0
  45. data/lib/never_bounce/api/response/jobs_results.rb +34 -0
  46. data/lib/never_bounce/api/response/jobs_results/item.rb +65 -0
  47. data/lib/never_bounce/api/response/jobs_results/query.rb +20 -0
  48. data/lib/never_bounce/api/response/jobs_search.rb +33 -0
  49. data/lib/never_bounce/api/response/jobs_search/query.rb +16 -0
  50. data/lib/never_bounce/api/response/jobs_search/result.rb +13 -0
  51. data/lib/never_bounce/api/response/jobs_start.rb +10 -0
  52. data/lib/never_bounce/api/response/jobs_status.rb +11 -0
  53. data/lib/never_bounce/api/response/message.rb +32 -0
  54. data/lib/never_bounce/api/response/single_check.rb +68 -0
  55. data/lib/never_bounce/api/response/status_message.rb +17 -0
  56. data/lib/never_bounce/api/response/success_message.rb +12 -0
  57. data/lib/never_bounce/api/session.rb +141 -0
  58. data/lib/never_bounce/api/version.rb +4 -0
  59. data/lib/neverbounce-api.rb +4 -0
  60. data/lib/neverbounce.rb +3 -0
  61. data/neverbounce-api.gemspec +20 -0
  62. data/spec/lib/never_bounce/api/client_spec.rb +199 -0
  63. data/spec/lib/never_bounce/api/feature/basic_initialize_spec.rb +25 -0
  64. data/spec/lib/never_bounce/api/feature/eigencache_spec.rb +28 -0
  65. data/spec/lib/never_bounce/api/feature/igetset_spec.rb +45 -0
  66. data/spec/lib/never_bounce/api/feature/oattrs_spec.rb +72 -0
  67. data/spec/lib/never_bounce/api/feature/require_attr_spec.rb +25 -0
  68. data/spec/lib/never_bounce/api/request/account_info_spec.rb +29 -0
  69. data/spec/lib/never_bounce/api/request/base_spec.rb +6 -0
  70. data/spec/lib/never_bounce/api/request/jobs_create_spec.rb +89 -0
  71. data/spec/lib/never_bounce/api/request/jobs_delete_spec.rb +31 -0
  72. data/spec/lib/never_bounce/api/request/jobs_download_spec.rb +31 -0
  73. data/spec/lib/never_bounce/api/request/jobs_parse_spec.rb +44 -0
  74. data/spec/lib/never_bounce/api/request/jobs_results_spec.rb +42 -0
  75. data/spec/lib/never_bounce/api/request/jobs_search_spec.rb +40 -0
  76. data/spec/lib/never_bounce/api/request/jobs_start_spec.rb +44 -0
  77. data/spec/lib/never_bounce/api/request/jobs_status_spec.rb +31 -0
  78. data/spec/lib/never_bounce/api/request/single_check_spec.rb +44 -0
  79. data/spec/lib/never_bounce/api/response/account_info/job_counts_spec.rb +7 -0
  80. data/spec/lib/never_bounce/api/response/account_info_spec.rb +9 -0
  81. data/spec/lib/never_bounce/api/response/address_info_spec.rb +7 -0
  82. data/spec/lib/never_bounce/api/response/base_spec.rb +6 -0
  83. data/spec/lib/never_bounce/api/response/container_spec.rb +142 -0
  84. data/spec/lib/never_bounce/api/response/credits_info/base_spec.rb +31 -0
  85. data/spec/lib/never_bounce/api/response/credits_info/monthly_spec.rb +11 -0
  86. data/spec/lib/never_bounce/api/response/credits_info/paid_spec.rb +11 -0
  87. data/spec/lib/never_bounce/api/response/feature/job_status_fields/total_spec.rb +7 -0
  88. data/spec/lib/never_bounce/api/response/feature/job_status_fields_spec.rb +42 -0
  89. data/spec/lib/never_bounce/api/response/jobs_create_spec.rb +7 -0
  90. data/spec/lib/never_bounce/api/response/jobs_delete_spec.rb +7 -0
  91. data/spec/lib/never_bounce/api/response/jobs_download_spec.rb +7 -0
  92. data/spec/lib/never_bounce/api/response/jobs_parse_spec.rb +7 -0
  93. data/spec/lib/never_bounce/api/response/jobs_results/jobs_results_item_spec.rb +7 -0
  94. data/spec/lib/never_bounce/api/response/jobs_results/jobs_results_query_spec.rb +7 -0
  95. data/spec/lib/never_bounce/api/response/jobs_results_spec.rb +7 -0
  96. data/spec/lib/never_bounce/api/response/jobs_search/jobs_search_query_spec.rb +7 -0
  97. data/spec/lib/never_bounce/api/response/jobs_search/jobs_search_result_spec.rb +7 -0
  98. data/spec/lib/never_bounce/api/response/jobs_search_spec.rb +7 -0
  99. data/spec/lib/never_bounce/api/response/jobs_start_spec.rb +7 -0
  100. data/spec/lib/never_bounce/api/response/jobs_status_spec.rb +7 -0
  101. data/spec/lib/never_bounce/api/response/single_check_spec.rb +7 -0
  102. data/spec/lib/never_bounce/api/response/spec_helper.rb +8 -0
  103. data/spec/lib/never_bounce/api/session_spec.rb +140 -0
  104. data/spec/lib/never_bounce/api_spec.rb +8 -0
  105. data/spec/spec_helper.rb +49 -0
  106. data/spec/spec_support/include_dir_context.rb +22 -0
  107. data/spec/spec_support/simplecov.rb +18 -0
  108. metadata +210 -0
@@ -0,0 +1,21 @@
1
+
2
+ module NeverBounce; module API
3
+ # Base class for all errors.
4
+ # @abstract
5
+ class Error < StandardError; end
6
+
7
+ # Incorrect attribute usage.
8
+ class AttributeError < Error; end
9
+
10
+ # Data format errors happening in our land.
11
+ class FormatError < Error; end
12
+
13
+ # Something went wrong with the request.
14
+ class RequestError < Error; end
15
+ end; end
16
+
17
+ #
18
+ # Implementation notes:
19
+ #
20
+ # * None of these exceptions should be suppressed in end-user script.
21
+ # Either they must be handled or they should crash the end-user script with a full backtrace.
@@ -0,0 +1,20 @@
1
+
2
+ module NeverBounce; module API; module Feature
3
+ # Provide the basic method <tt>#initialize</tt> to the owner class.
4
+ # @see InstanceMethods#initialize
5
+ module BasicInitialize
6
+ # @param owner [Class]
7
+ # @return [nil]
8
+ def self.load(owner)
9
+ return if owner < InstanceMethods
10
+ owner.send(:include, InstanceMethods)
11
+ end
12
+
13
+ module InstanceMethods
14
+ # See source for details.
15
+ def initialize(attrs = {})
16
+ attrs.each { |k, v| public_send("#{k}=", v) }
17
+ end
18
+ end
19
+ end
20
+ end; end; end
@@ -0,0 +1,48 @@
1
+
2
+ module NeverBounce; module API; module Feature
3
+ # Eigenclass (singleton class)-based cache which allows to handle "hidden" instance variables.
4
+ # @see InstanceMethods#_cache
5
+ module Eigencache
6
+ # @param owner [Class]
7
+ # @return [nil]
8
+ def self.load(owner)
9
+ return if owner < InstanceMethods
10
+ owner.send(:include, InstanceMethods)
11
+ end
12
+
13
+ module InstanceMethods
14
+ # Hidden cache.
15
+ #
16
+ # def body
17
+ # _cache[:body] ||= File.read("my-bulky-body.csv")
18
+ # end
19
+ #
20
+ # def body=(v)
21
+ # _cache[:body] = v
22
+ # end
23
+ #
24
+ # @return [Hash]
25
+ def _cache
26
+ if eigen.instance_variable_defined?(k = :@cache)
27
+ eigen.instance_variable_get(k)
28
+ else
29
+ eigen.instance_variable_set(k, {})
30
+ end
31
+ end
32
+
33
+ #---------------------------------------
34
+ private
35
+
36
+ # Object's eigenclass (singleton class).
37
+ # @return [Class]
38
+ def eigen
39
+ class << self; self; end
40
+ end
41
+ end
42
+ end
43
+ end; end; end
44
+
45
+ #
46
+ # Implementation notes:
47
+ #
48
+ # * As of Ruby 2.1 `Object#singleton_class` is introduced. We use our own compatible implementation, `eigen`.
@@ -0,0 +1,41 @@
1
+
2
+ module NeverBounce; module API; module Feature
3
+ # @see InstanceMethods#igetset
4
+ module Igetset
5
+ # @param owner [Class]
6
+ # @return [nil]
7
+ def self.load(owner)
8
+ return if owner < InstanceMethods
9
+ owner.send(:include, InstanceMethods)
10
+ end
11
+
12
+ module InstanceMethods
13
+ private
14
+
15
+ # Get/set an OTF instance variable of any type.
16
+ #
17
+ # Ruby's <tt>||=</tt> works nicely with object instances, but requires special bulky treatment for <tt>nil</tt> and <tt>false</tt>.
18
+ # For example, this will cause a hidden glitch since <tt>==</tt> can evaluate to <tt>false</tt>:
19
+ #
20
+ # @is_verbose ||= begin
21
+ # # This clause will be evaluated *every time* if its value is `false`.
22
+ # ENV["VERBOSE"] == "y"
23
+ # end
24
+ #
25
+ # There's a number of solutions to this problem, all of which involve calling <tt>instance_variable_*</tt> a few times per attribute accessor.
26
+ #
27
+ # <tt>igetset</tt> does this job for you. All you have to do is specify a block to compute the value.
28
+ #
29
+ # igetset(:is_verbose) { ENV["VERBOSE"] == "y" }
30
+ #
31
+ # See source for details.
32
+ def igetset(name, &compute)
33
+ if instance_variable_defined?(k = "@#{name}")
34
+ instance_variable_get(k)
35
+ else
36
+ instance_variable_set(k, compute.call)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end; end; end
@@ -0,0 +1,100 @@
1
+
2
+ module NeverBounce; module API; module Feature
3
+ # Declare and manage on-the-fly (OTF) attributes of the class, codenamed "oattrs".
4
+ # @see ClassMethods#oattr
5
+ # @see ClassMethods#oattrs
6
+ # @see InstanceMethods#touch
7
+ module Oattrs
8
+ # @return [nil]
9
+ def self.load(owner)
10
+ return if owner < InstanceMethods
11
+ owner.extend(ClassMethods)
12
+ owner.send(:include, InstanceMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+ # Return oattrs declared so far, recurse all superclasses.
17
+ #
18
+ # oattrs # => [:first_name, :last_name]
19
+ #
20
+ # @!attribute oattrs
21
+ # @return [Array]
22
+ def oattrs
23
+ @attrs ||= if superclass.respond_to?(m = :oattrs)
24
+ superclass.send(m).dup
25
+ else
26
+ []
27
+ end
28
+ end
29
+
30
+ def oattrs=(ar)
31
+ @oattrs = ar
32
+ end
33
+
34
+ # Declare an OTF attribute.
35
+ #
36
+ # class Klass
37
+ # oattr :first_name, :writer
38
+ # oattr :goods, :custom
39
+ #
40
+ # def first_name
41
+ # @first_name ||= ENV["FIRST_NAME"]
42
+ # end
43
+ #
44
+ # def goods
45
+ # @goods ||= ...
46
+ #
47
+ # def goods=(ar)
48
+ # @goods = ar
49
+ # ...
50
+ #
51
+ # @return [Symbol] <tt>name</tt>.
52
+ # @see InstanceMethods#touch
53
+ def oattr(name, type)
54
+ case type
55
+ when :custom
56
+ # Do nothing, just register attr below.
57
+ when :writer
58
+ attr_writer name
59
+ else
60
+ raise ArgumentError, "Unknown type: #{type.inspect}"
61
+ end
62
+
63
+ # Register and return.
64
+ name.tap { oattrs << name}
65
+ end
66
+ end
67
+
68
+ module InstanceMethods
69
+ # Load all oattrs by "touching" them.
70
+ #
71
+ # irb> resp = client.jobs_delete(job_id: 353701)
72
+ # => #<NeverBounce::API::Response::ErrorMessage:0x0056245978aec8>
73
+ # irb> resp.touch
74
+ # => #<NeverBounce::API::Response::ErrorMessage:0x0056245978aec8 @message="Invalid job ID 353701", @execution_time=15, @status="general_failure">
75
+ #
76
+ # @return [self]
77
+ def touch
78
+ self.class.oattrs.each do |name|
79
+ v = public_send(name)
80
+
81
+ # Touch recursively. Support simple collections.
82
+ if v.respond_to? :touch
83
+ v.touch
84
+ elsif v.is_a? Array
85
+ v.each { |r| r.touch if r.respond_to? :touch }
86
+ end
87
+ end
88
+
89
+ self
90
+ end
91
+ end
92
+ end
93
+ end; end; end
94
+
95
+ #
96
+ # Implementation notes:
97
+ #
98
+ # * Originally I've used `attr` and `attrs` as method names.
99
+ # Sadly enough, YARD choked on `attr ...` with no reasonable way to work around it and tell it to ignore these statements.
100
+ # That led me to the conclusion that `attr` acts more like a language construct, so I'd rather not override it, but make my own.
@@ -0,0 +1,27 @@
1
+
2
+ require "never_bounce/api/error"
3
+
4
+ module NeverBounce; module API; module Feature
5
+ # Provide attribute validation method(s) to the owner class.
6
+ # @see InstanceMethods#initialize
7
+ module RequireAttr
8
+ # @param owner [Class]
9
+ # @return [nil]
10
+ def self.load(owner)
11
+ return if owner < InstanceMethods
12
+ owner.send(:include, InstanceMethods)
13
+ end
14
+
15
+ module InstanceMethods
16
+ private
17
+
18
+ # Require attribute to be set. Return attribute value.
19
+ # @return [mixed]
20
+ def require_attr(name)
21
+ send(name).tap do |_|
22
+ raise AttributeError, "Attribute must be set: #{name}" if _.nil?
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end; end; end
@@ -0,0 +1,29 @@
1
+
2
+ require "never_bounce/api/response/account_info"
3
+
4
+ require_relative "base"
5
+
6
+ module NeverBounce; module API; module Request
7
+ class AccountInfo < Base
8
+ # @return [Symbol]
9
+ def self.http_method
10
+ :get
11
+ end
12
+
13
+ # @return [String]
14
+ def self.path
15
+ "account/info"
16
+ end
17
+
18
+ # @return [Response::AccountInfo]
19
+ def self.response_klass
20
+ Response::AccountInfo
21
+ end
22
+
23
+ def to_h
24
+ {
25
+ key: require_attr(:api_key),
26
+ }
27
+ end
28
+ end
29
+ end; end; end
@@ -0,0 +1,102 @@
1
+
2
+ require "never_bounce/api/feature/basic_initialize"
3
+ require "never_bounce/api/feature/require_attr"
4
+
5
+ module NeverBounce; module API; module Request
6
+ # @abstract
7
+ # @see API::Feature::BasicInitialize
8
+ class Base
9
+ API::Feature::BasicInitialize.load(self)
10
+ API::Feature::RequireAttr.load(self)
11
+
12
+ # User's API key.
13
+ # @return [String]
14
+ attr_accessor :api_key
15
+ attr_writer :api_url, :headers, :user_agent
16
+
17
+ # Custom API URL. Default is <tt>https://api.neverbounce.com/v4</tt>.
18
+ # @return [String]
19
+ def api_url
20
+ @api_url ||= "https://api.neverbounce.com/v4"
21
+ end
22
+
23
+ # @!attribute headers
24
+ # @return [Array]
25
+ def headers
26
+ {
27
+ "Content-Type" => "application/json",
28
+ "User-Agent" => user_agent,
29
+ }
30
+ end
31
+
32
+ # <tt>:get</tt>, <tt>:post</tt> or whatever.
33
+ # @abstract
34
+ # @return [Symbol]
35
+ # @return [String]
36
+ def self.http_method
37
+ raise NotImplementedError, "Redefine `self.http_method` in your class: #{self}"
38
+ end
39
+
40
+ # Request path on server, e.g. <tt>"jobs/parse"</tt>.
41
+ # @abstract
42
+ # @return [String]
43
+ def self.path
44
+ raise NotImplementedError, "Redefine `self.path` in your class: #{self}"
45
+ end
46
+
47
+ # @abstract
48
+ # @return [Class]
49
+ def self.response_klass
50
+ raise NotImplementedError, "Redefine `self.response_klass` in your class: #{self}"
51
+ end
52
+
53
+ # Build arguments for cURL OS command.
54
+ # @return [Array]
55
+ def to_curl
56
+ # NOTE: I consider we should use long options to avoid ambiguity of ones like `-u` etc.
57
+ @curl ||= begin
58
+ ar = [
59
+ "--request", self.class.http_method.to_s.upcase,
60
+ "--url", "#{api_url}/#{self.class.path}",
61
+ ]
62
+
63
+ ar += headers.reject { |k,| k == "User-Agent" }.flat_map do |k, v|
64
+ ["--header", "#{k}: #{v}"]
65
+ end
66
+
67
+ ar += ["--data-binary", to_h.to_json]
68
+
69
+ ar
70
+ end
71
+ end
72
+
73
+ # Build a <tt>Hash</tt> representation of request data.
74
+ # @abstract
75
+ # @return [Hash]
76
+ def to_h
77
+ raise NotImplementedError, "Redefine `to_h` in your class: #{self.class}"
78
+ end
79
+
80
+ # Build argumentsfor <tt>Httparty</tt> invocation.
81
+ # @return [Array]
82
+ def to_httparty
83
+ [
84
+ self.class.http_method, # E.g. `:get`.
85
+ "#{api_url}/#{self.class.path}",
86
+ {
87
+ body: to_h.to_json,
88
+ headers: headers,
89
+ }
90
+ ]
91
+ end
92
+
93
+ # @!attribute user_agent
94
+ # @return [String]
95
+ def user_agent
96
+ @user_agent ||= [
97
+ "NeverBounceApi-Ruby/#{API::VERSION} (#{RUBY_PLATFORM})",
98
+ "Ruby/#{RUBY_VERSION} (p #{RUBY_PATCHLEVEL}; rev #{RUBY_REVISION})",
99
+ ].join(" ")
100
+ end
101
+ end
102
+ end; end; end
@@ -0,0 +1,90 @@
1
+
2
+ require_relative "base"
3
+
4
+ module NeverBounce; module API; module Request
5
+ class JobsCreate < Base
6
+ # @return [Boolean]
7
+ attr_accessor :auto_parse
8
+
9
+ # @return [Boolean]
10
+ attr_accessor :auto_start
11
+
12
+ # @return [String]
13
+ attr_accessor :filename
14
+
15
+ # Input specification.
16
+ # @return [String] URL if <tt>input_location</tt> is <tt>"remote".
17
+ # @return [Array<Array<email, name>>] Structure if <tt>input_location</tt> is "supplied".
18
+ attr_accessor :input
19
+
20
+ # @return [String]
21
+ attr_accessor :input_location
22
+
23
+ # @return [Boolean]
24
+ attr_accessor :run_sample
25
+
26
+ # @return [Symbol]
27
+ def self.http_method
28
+ :post
29
+ end
30
+
31
+ # @return [String]
32
+ def self.path
33
+ "jobs/create"
34
+ end
35
+
36
+ # @return [Response::JobsCreate]
37
+ def self.response_klass
38
+ Response::JobsCreate
39
+ end
40
+
41
+ # Return a ready-to-merge mode attributes hash.
42
+ # @return [Hash]
43
+ def mode_h
44
+ @mode_h ||= {}.tap do |_|
45
+ unless (v = auto_start).nil?
46
+ _[:auto_start] = v
47
+ end
48
+
49
+ unless (v = auto_parse).nil?
50
+ _[:auto_parse] = v
51
+ end
52
+
53
+ unless (v = run_sample).nil?
54
+ _[:run_sample] = v
55
+ end
56
+ end
57
+ end
58
+
59
+ # @return [Hash]
60
+ def to_h
61
+ input = require_attr(:input)
62
+ input_location = require_attr(:input_location)
63
+
64
+ # Validate `input_location` and `input`.
65
+
66
+ if not ["remote_url", "supplied"].include? input_location
67
+ raise AttributeError, "Unknown `input_location`: #{input_location.inspect}"
68
+ end
69
+
70
+ if input_location == "supplied"
71
+ raise AttributeError, "Invalid `input` for `input_location` == #{input_location.inspect}: #{input.inspect}" if not input.is_a? Array
72
+
73
+ # Skim through elements of `input`, raise if invalid structure detected, with details.
74
+ input.each do |elem|
75
+ em = [AttributeError, "Invalid `input` element: #{elem.inspect}"]
76
+ raise(*em) unless elem.is_a? Array
77
+ raise(*em) unless elem.map(&:class) == [String, String]
78
+ end
79
+ end
80
+
81
+ # Result.
82
+ {
83
+ input: input,
84
+ input_location: input_location,
85
+ filename: require_attr(:filename),
86
+ key: require_attr(:api_key),
87
+ }.merge(mode_h)
88
+ end
89
+ end
90
+ end; end; end