bitly 1.1.1 → 2.0.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.
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