bitly 1.1.1 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +36 -3
  3. data/.rspec +3 -0
  4. data/.travis.yml +6 -2
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +5 -2
  7. data/History.txt +32 -1
  8. data/LICENSE.md +1 -1
  9. data/README.md +151 -58
  10. data/Rakefile +6 -9
  11. data/bitly.gemspec +36 -32
  12. data/config/env.yml.example +5 -0
  13. data/lib/bitly.rb +9 -7
  14. data/lib/bitly/api.rb +19 -0
  15. data/lib/bitly/api/base.rb +23 -0
  16. data/lib/bitly/api/bitlink.rb +342 -0
  17. data/lib/bitly/api/bitlink/clicks_summary.rb +35 -0
  18. data/lib/bitly/api/bitlink/deeplink.rb +29 -0
  19. data/lib/bitly/api/bitlink/link_click.rb +75 -0
  20. data/lib/bitly/api/bitlink/paginated_list.rb +52 -0
  21. data/lib/bitly/api/bsd.rb +24 -0
  22. data/lib/bitly/api/click_metric.rb +186 -0
  23. data/lib/bitly/api/client.rb +588 -0
  24. data/lib/bitly/api/group.rb +232 -0
  25. data/lib/bitly/api/group/preferences.rb +73 -0
  26. data/lib/bitly/api/list.rb +22 -0
  27. data/lib/bitly/api/oauth_app.rb +26 -0
  28. data/lib/bitly/api/organization.rb +104 -0
  29. data/lib/bitly/api/shorten_counts.rb +61 -0
  30. data/lib/bitly/api/user.rb +107 -0
  31. data/lib/bitly/error.rb +33 -0
  32. data/lib/bitly/http.rb +10 -0
  33. data/lib/bitly/http/adapters.rb +9 -0
  34. data/lib/bitly/http/adapters/net_http.rb +27 -0
  35. data/lib/bitly/http/client.rb +33 -0
  36. data/lib/bitly/http/request.rb +118 -0
  37. data/lib/bitly/http/response.rb +66 -0
  38. data/lib/bitly/oauth.rb +109 -0
  39. data/lib/bitly/version.rb +3 -1
  40. metadata +82 -111
  41. data/Manifest +0 -37
  42. data/lib/bitly/client.rb +0 -145
  43. data/lib/bitly/config.rb +0 -29
  44. data/lib/bitly/url.rb +0 -103
  45. data/lib/bitly/utils.rb +0 -57
  46. data/lib/bitly/v3.rb +0 -14
  47. data/lib/bitly/v3/bitly.rb +0 -7
  48. data/lib/bitly/v3/client.rb +0 -207
  49. data/lib/bitly/v3/country.rb +0 -13
  50. data/lib/bitly/v3/day.rb +0 -13
  51. data/lib/bitly/v3/missing_url.rb +0 -15
  52. data/lib/bitly/v3/oauth.rb +0 -41
  53. data/lib/bitly/v3/realtime_link.rb +0 -18
  54. data/lib/bitly/v3/referrer.rb +0 -13
  55. data/lib/bitly/v3/url.rb +0 -154
  56. data/lib/bitly/v3/user.rb +0 -135
  57. data/test/bitly/test_client.rb +0 -266
  58. data/test/bitly/test_config.rb +0 -28
  59. data/test/bitly/test_url.rb +0 -167
  60. data/test/bitly/test_utils.rb +0 -79
  61. data/test/fixtures/cnn.json +0 -1
  62. data/test/fixtures/cnn_and_google.json +0 -1
  63. data/test/fixtures/expand_cnn.json +0 -1
  64. data/test/fixtures/expand_cnn_and_google.json +0 -1
  65. data/test/fixtures/google_and_cnn_info.json +0 -1
  66. data/test/fixtures/google_info.json +0 -1
  67. data/test/fixtures/google_stats.json +0 -1
  68. data/test/fixtures/shorten_error.json +0 -1
  69. data/test/test_helper.rb +0 -39
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./base"
3
+
4
+ module Bitly
5
+ module API
6
+ class ShortenCounts
7
+ include Base
8
+
9
+ def self.attributes
10
+ [:units, :facet, :unit_reference, :unit]
11
+ end
12
+
13
+ attr_reader(*attributes)
14
+ attr_reader :metrics
15
+
16
+ Metric = Struct.new(:key, :value)
17
+
18
+ ##
19
+ # Shorten counts by group
20
+ # [`GET /v4/groups/{group_guid}/shorten_counts`](https://dev.bitly.com/v4/#operation/getGroupShortenCounts)
21
+ #
22
+ # @example
23
+ # shorten_counts = Bitly::API::ShortenCounts.by_group(client: client, group_guid: group_guid)
24
+ #
25
+ # @param client [Bitly::API::Client] An authorized API client
26
+ # @param group_guid [String] The guid of the group for which you want
27
+ # shorten counts
28
+ #
29
+ # @return [Bitly::API::ShortenCounts]
30
+ def self.by_group(client:, group_guid:)
31
+ response = client.request(path: "/groups/#{group_guid}/shorten_counts")
32
+ new(data: response.body, response: response)
33
+ end
34
+
35
+ ##
36
+ # Shorten counts by organization
37
+ # [`GET /v4/organizations/{organization_guid}/shorten_counts`](https://dev.bitly.com/v4/#operation/getOrganizationShortenCounts)
38
+ #
39
+ # @example
40
+ # shorten_counts = Bitly::API::ShortenCounts.by_organization(client: client, organization_guid: organization_guid)
41
+ #
42
+ # @param client [Bitly::API::Client] An authorized API client
43
+ # @param organization_guid [String] The guid of the organization for which
44
+ # you want shorten counts
45
+ #
46
+ # @return [Bitly::API::ShortenCounts]
47
+ def self.by_organization(client:, organization_guid:)
48
+ response = client.request(path: "/organizations/#{organization_guid}/shorten_counts")
49
+ new(data: response.body, response: response)
50
+ end
51
+
52
+ def initialize(data:, response: nil)
53
+ assign_attributes(data)
54
+ @metrics = data["metrics"].map do |metric|
55
+ Metric.new(metric["key"], metric["value"])
56
+ end
57
+ @response = response
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+ require_relative "./base"
3
+
4
+ module Bitly
5
+ module API
6
+ ##
7
+ # A User represents the authorized user
8
+ class User
9
+ class Email
10
+ attr_reader :email, :is_verified, :is_primary
11
+ def initialize(data)
12
+ @email = data["email"]
13
+ @is_verified = data["is_verified"]
14
+ @is_primary = data["is_primary"]
15
+ end
16
+ end
17
+
18
+ include Base
19
+ ##
20
+ # Gets the authorized user from the API.
21
+ # [`GET /v4/user`](https://dev.bitly.com/v4/#operation/getUser)
22
+ #
23
+ # @example
24
+ # user = Bitly::API::User.fetch(client: client)
25
+ #
26
+ # @param client [Bitly::API::Client] The authorized API client
27
+ #
28
+ # @return [Bitly::API::User]
29
+ def self.fetch(client:)
30
+ response = client.request(path: "/user")
31
+ new(data: response.body, client: client, response: response)
32
+ end
33
+
34
+ # @return [Array<Symbol>] The attributes the API returns for a user
35
+ def self.attributes
36
+ [:login, :is_active, :is_2fa_enabled, :name, :is_sso_user, :default_group_guid]
37
+ end
38
+ # @return [Array<Symbol>] The attributes the API returns that need to be
39
+ # converted to `Time` objects.
40
+ def self.time_attributes
41
+ [:created, :modified]
42
+ end
43
+
44
+ attr_reader(*(attributes + time_attributes))
45
+ attr_reader :emails
46
+
47
+ ##
48
+ # Creates a Bitly::API::User object.
49
+ #
50
+ # @example
51
+ # user = Bitly::API::User.new(data: user_data, client: client)
52
+ #
53
+ # @param data [Hash<String, String | Boolean>] The user data from the API
54
+ # @param client [Bitly::API::Client] The authorized API client
55
+ # @param response [Bitly::HTTP::Response] The original HTTP response
56
+ #
57
+ # @return [Bitly::API::User]
58
+ def initialize(data:, client:, response: nil)
59
+ assign_attributes(data)
60
+ @client = client
61
+ @response = response
62
+ if data["emails"]
63
+ @emails = data["emails"].map { |e| Email.new(e) }
64
+ end
65
+ end
66
+
67
+ ##
68
+ # Returns the default group for the user from the default group guid
69
+ #
70
+ # @example
71
+ # user.default_group
72
+ #
73
+ # @returns [Bitly::API::Group]
74
+ def default_group
75
+ @default_group ||= Group.fetch(client: @client, guid: default_group_guid)
76
+ end
77
+
78
+ ##
79
+ # Allows you to update the authorized user's name or default group guid.
80
+ # If you update the default group ID and have already loaded the default
81
+ # group, it is nilled out so it can be reloaded with the correct ID.
82
+ # [`PATCH /v4/user`](https://dev.bitly.com/v4/#operation/updateUser)]
83
+ #
84
+ # @example
85
+ # user.update(name: "New Name", default_group_guid: "aaabbb")
86
+ #
87
+ # @param name [String] A new name
88
+ # @param default_group_guid [String] A new default guid
89
+ #
90
+ # @return [Bitly::API::User]
91
+ def update(name: nil, default_group_guid: nil)
92
+ params = { "name" => name }
93
+ if default_group_guid
94
+ params["default_group_guid"] = default_group_guid
95
+ @default_group = nil
96
+ end
97
+ @response = @client.request(
98
+ path: "/user",
99
+ method: "PATCH",
100
+ params: params
101
+ )
102
+ assign_attributes(@response.body)
103
+ self
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitly
4
+ ##
5
+ # An error class that covers all potential errors from the Bitly API. In an
6
+ # error scenario, the API is only guaranteed to return a status_code and
7
+ # status_txt: https://dev.bitly.com/formats.html
8
+ class Error < StandardError
9
+ ##
10
+ # @return [String] The status code of the failed request
11
+ attr_reader :status_code
12
+
13
+ ##
14
+ # @return [String] The description of the failed request
15
+ attr_reader :description
16
+
17
+ ##
18
+ # @return [Bitly::HTTP::Response] The response that caused the error
19
+ attr_reader :response
20
+
21
+ ##
22
+ # Creates a new Bitly::Error object
23
+ #
24
+ # @param [Bitly::HTTP::Response] response The parsed response to the HTTP request
25
+ def initialize(response)
26
+ @response = response
27
+ @status_code = response.status
28
+ @description = response.body["description"]
29
+ @message = "[#{@status_code}] #{response.body["message"]}"
30
+ super(@message)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitly
4
+ module HTTP
5
+ autoload :Adapters, File.join(File.dirname(__FILE__), "http/adapters.rb")
6
+ autoload :Response, File.join(File.dirname(__FILE__), "http/response.rb")
7
+ autoload :Request, File.join(File.dirname(__FILE__), "http/request.rb")
8
+ autoload :Client, File.join(File.dirname(__FILE__), "http/client.rb")
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitly
4
+ module HTTP
5
+ module Adapters
6
+ autoload :NetHTTP, File.join(File.dirname(__FILE__), "adapters/net_http.rb")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module Bitly
6
+ module HTTP
7
+ module Adapters
8
+ class NetHTTP
9
+ def request(request)
10
+ Net::HTTP.start(request.uri.host, request.uri.port, use_ssl: true) do |http|
11
+ method = Object.const_get("Net::HTTP::#{request.method.capitalize}")
12
+ full_path = request.uri.path
13
+ full_path += "?#{request.uri.query}" if request.uri.query
14
+ http_request = method.new full_path
15
+ http_request.body = request.body
16
+ request.headers.each do |header, value|
17
+ http_request[header] = value
18
+ end
19
+ response = http.request http_request
20
+ success = response.kind_of? Net::HTTPSuccess
21
+ return [response.code, response.body, response.to_hash, success]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitly
4
+ module HTTP
5
+ class Client
6
+ def initialize(adapter=Bitly::HTTP::Adapters::NetHTTP.new)
7
+ @adapter = adapter
8
+ raise ArgumentError, "Adapter must have a request method." unless @adapter.respond_to?(:request)
9
+ end
10
+
11
+ ##
12
+ # The main method for the HTTP client. It receives a Bitly::HTTP::Request
13
+ # object, makes the request described and returns a Bitly::HTTP::Response.
14
+ #
15
+ # @param [Bitly::HTTP::Request] request The request that should be made
16
+ #
17
+ # @return [Bitly::HTTP::Response] The response from the request.
18
+ #
19
+ # @raise [Bitly::Error] If the response is not a successful response
20
+ # in the 2xx range, then we raise an error with the response passed as
21
+ # an argument. It is up to the application to catch this error.
22
+ def request(request)
23
+ status, body, headers, success = @adapter.request(request)
24
+ response = Bitly::HTTP::Response.new(status: status, body: body, headers: headers, request: request)
25
+ if success
26
+ return response
27
+ else
28
+ raise Bitly::Error, response
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+ require "uri"
4
+
5
+ module Bitly
6
+ module HTTP
7
+ class Request
8
+ # @return [String] The HTTP method that the request should be.
9
+ attr_reader :method
10
+
11
+ # @return [Hash] A hash of parameters that will be turned into query
12
+ # parameters or a request body
13
+ attr_reader :params
14
+
15
+ # @return [Hash] A hash of HTTP headers that will be included with the
16
+ # request
17
+ attr_reader :headers
18
+
19
+ ##
20
+ # Creates a new Bitly::HTTP::Request object, which is to be used by the
21
+ # [Bitly::HTTP::Client].
22
+ #
23
+ # @example
24
+ # request = Bitly::HTTP::Request.new(uri: URI.parse('https://api-ssl.bitly.com/v3/shorten'), method: "GET")
25
+ #
26
+ # @param [URI] uri A [URI] that you want to make the request to.
27
+ # @param [String] method The HTTP method that should be used to make the
28
+ # request.
29
+ # @param [Hash] params The parameters to be sent as part of the request.
30
+ # GET parameters will be sent as part of the query string and other
31
+ # methods will be added to the request body.
32
+ def initialize(uri: , method: "GET", params: {}, headers: {})
33
+ errors = []
34
+ @uri = uri
35
+ errors << "uri must be an object of type URI. Received a #{uri.class}" unless uri.kind_of?(URI)
36
+ @method = method
37
+ errors << "method must be a valid HTTP method. Received: #{method}." unless HTTP_METHODS.include?(method)
38
+ @params = params
39
+ errors << "params must be a hash. Received: #{params.inspect}." unless params.kind_of?(Hash)
40
+ @headers = headers
41
+ errors << "headers must be a hash. Received: #{headers.inspect}." unless headers.kind_of?(Hash)
42
+ raise ArgumentError, errors.join("\n") if errors.any?
43
+ end
44
+
45
+ ##
46
+ # Returns the uri for the request. If the request is an HTTP method that
47
+ # uses a body to send data, then the uri is the one that the request was
48
+ # initialised with. If the request uses query parameters, then the
49
+ # parameters are serialised and added to the uri's query.
50
+ #
51
+ # @example
52
+ # uri = URI.parse("https://api-ssl.bitly.com/v3/shorten")
53
+ # request = Bitly::HTTP::Request.new(uri: uri, params: { foo: "bar" })
54
+ # request.uri.to_s
55
+ # # => "https://api-ssl.bitly.com/v3/shorten?foo=bar"
56
+ #
57
+ # @return [URI] The full URI for the request
58
+ def uri
59
+ uri = @uri.dup
60
+ return uri if HTTP_METHODS_WITH_BODY.include?(@method)
61
+ if uri.query
62
+ existing_query = URI.decode_www_form(uri.query)
63
+ new_query = hash_to_arrays(@params)
64
+ uri.query = URI.encode_www_form((existing_query + new_query).uniq)
65
+ else
66
+ uri.query = URI.encode_www_form(@params) if @params.any?
67
+ end
68
+ uri
69
+ end
70
+
71
+ ##
72
+ # Returns the body of the request if the request is an HTTP method that
73
+ # uses a body to send data. The body is a JSON string of the parameters.
74
+ # If the request doesn't use a body to send data, this returns nil.
75
+ #
76
+ # @example
77
+ # uri = URI.parse("https://api-ssl.bitly.com/v3/shorten")
78
+ # request = Bitly::HTTP::Request.new(uri: uri, method: 'POST', params: { foo: "bar" })
79
+ # request.body
80
+ # # => "{\"foo\":\"bar\"}"
81
+ #
82
+ # @return [String] The request body
83
+ def body
84
+ return nil if HTTP_METHODS_WITHOUT_BODY.include?(@method)
85
+ return JSON.generate(params)
86
+ end
87
+
88
+ private
89
+
90
+ def hash_to_arrays(hash)
91
+ hash.map do |key, value|
92
+ if value.is_a?(Array)
93
+ value.map { |v| [key, v] }
94
+ else
95
+ [[key, value]]
96
+ end
97
+ end.flatten(1)
98
+ end
99
+
100
+ HTTP_METHODS_WITHOUT_BODY = [
101
+ "GET",
102
+ "HEAD",
103
+ "DELETE",
104
+ "TRACE",
105
+ "OPTIONS"
106
+ ]
107
+
108
+ HTTP_METHODS_WITH_BODY = [
109
+ "POST",
110
+ "PUT",
111
+ "PATCH",
112
+ "CONNECT"
113
+ ]
114
+
115
+ HTTP_METHODS = HTTP_METHODS_WITH_BODY + HTTP_METHODS_WITHOUT_BODY
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ require "json"
3
+
4
+ module Bitly
5
+ module HTTP
6
+ ##
7
+ # The Response class handles generic responses from the API. It is made up
8
+ # of a status code, body and headers. The body is expected to be JSON and it
9
+ # will parse the body. The status should lie within the range 100 - 599 and
10
+ # the headers should be a hash.
11
+ class Response
12
+
13
+ # @return [String] The response's status code
14
+ attr_reader :status
15
+
16
+ # @return [Hash] The response's parsed body
17
+ attr_reader :body
18
+
19
+ # @return [Hash] The response's headers
20
+ attr_reader :headers
21
+
22
+ # @return [Bitly::HTTP::Request] The request that caused this response
23
+ attr_reader :request
24
+
25
+ ##
26
+ # Creates a new Bitly::HTTP::Response object, which can be used by other
27
+ # objects in the library.
28
+ #
29
+ # @example
30
+ # response = Bitly::HTTP::Response.new(status: "200", body: "{}", headers: {})
31
+ #
32
+ # @param [String] status The status code of the response, which should be
33
+ # between 100 and 599
34
+ # @param [String] body The body of the response, a String that is valid
35
+ # JSON and will be parsed
36
+ # @param [Hash] headers The response headers
37
+ def initialize(status:, body:, headers:, request: nil)
38
+ errors = []
39
+ @status = status
40
+ errors << "Status must be a valid HTTP status code. Received #{status}" unless is_status?(status)
41
+ if body.nil? || body.empty?
42
+ @body = nil
43
+ else
44
+ begin
45
+ @body = JSON.parse(body)
46
+ rescue JSON::ParserError
47
+ @body = {
48
+ "message" => body
49
+ }
50
+ end
51
+ end
52
+ @headers = headers
53
+ errors << "Headers must be a hash. Received #{headers}" unless headers.is_a?(Hash)
54
+ @request = request
55
+ errors << "Request must be a Bitly::HTTP::Request. Received #{request}" if request && !request.is_a?(Request)
56
+ raise ArgumentError, errors.join("\n"), caller if errors.any?
57
+ end
58
+
59
+ private
60
+
61
+ def is_status?(status)
62
+ !!status.match(/\A[1-5][0-9][0-9]\z/)
63
+ end
64
+ end
65
+ end
66
+ end