fabricio 1.0.0

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 (37) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.idea/runConfigurations/IRB_console__fabricio.xml +23 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +10 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +4 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +149 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/docs/api_reference.md +611 -0
  15. data/docs/swagger-api.json +553 -0
  16. data/fabricio.gemspec +29 -0
  17. data/lib/fabricio.rb +2 -0
  18. data/lib/fabricio/authorization/abstract_session_storage.rb +26 -0
  19. data/lib/fabricio/authorization/authorization_client.rb +122 -0
  20. data/lib/fabricio/authorization/memory_session_storage.rb +35 -0
  21. data/lib/fabricio/authorization/session.rb +21 -0
  22. data/lib/fabricio/client/client.rb +92 -0
  23. data/lib/fabricio/models/abstract_model.rb +17 -0
  24. data/lib/fabricio/models/app.rb +24 -0
  25. data/lib/fabricio/models/build.rb +23 -0
  26. data/lib/fabricio/models/organization.rb +22 -0
  27. data/lib/fabricio/models/point.rb +17 -0
  28. data/lib/fabricio/networking/app_request_model_factory.rb +229 -0
  29. data/lib/fabricio/networking/build_request_model_factory.rb +103 -0
  30. data/lib/fabricio/networking/network_client.rb +101 -0
  31. data/lib/fabricio/networking/organization_request_model_factory.rb +27 -0
  32. data/lib/fabricio/networking/request_model.rb +39 -0
  33. data/lib/fabricio/services/app_service.rb +146 -0
  34. data/lib/fabricio/services/build_service.rb +59 -0
  35. data/lib/fabricio/services/organization_service.rb +33 -0
  36. data/lib/fabricio/version.rb +3 -0
  37. metadata +163 -0
data/fabricio.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fabricio/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fabricio"
8
+ spec.version = Fabricio::VERSION
9
+ spec.authors = ["Egor Tolstoy"]
10
+ spec.email = ["e.tolstoy@rambler-co.ru"]
11
+
12
+ spec.summary = "A simple gem that fetches mobile application statistics from Fabric.io API."
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
16
+ f.match(%r{^(test|spec|features)/})
17
+ end
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency 'faraday'
23
+
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ spec.add_development_dependency 'webmock'
27
+ spec.add_development_dependency 'simplecov'
28
+ spec.add_development_dependency 'codeclimate-test-reporter', '~> 1.0.0'
29
+ end
data/lib/fabricio.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'fabricio/version'
2
+ require 'fabricio/client/client'
@@ -0,0 +1,26 @@
1
+ require 'fabricio/authorization/session'
2
+
3
+ module Fabricio
4
+ module Authorization
5
+ # A class providing an interface for implementing Fabric session storage. Subclass it to provide your own behaviour (e.g. storing session data in database)
6
+ class AbstractSessionStorage
7
+
8
+ # Override it with your own behavior of obtaining a [Fabricio::Authorization::Session] object
9
+ #
10
+ # @return [Fabricio::Authorization::Session]
11
+ def obtain_session
12
+ raise NotImplementedError, "Implement this method in a child class"
13
+ end
14
+
15
+ # Override it with your own behavior of storing a [Fabricio::Authorization::Session] object
16
+ def store_session(_)
17
+ raise NotImplementedError, "Implement this method in a child class"
18
+ end
19
+
20
+ # Override it with your own behavior of deleting stored [Fabricio::Authorization::Session] object
21
+ def reset
22
+ raise NotImplementedError, "Implement this method in a child class"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,122 @@
1
+ require 'faraday'
2
+ require 'json'
3
+ require 'fabricio/authorization/session'
4
+ require 'fabricio/services/organization_service'
5
+
6
+ AUTH_API_URL = 'https://fabric.io/oauth/token'
7
+ ORGANIZATION_API_URL = 'https://fabric.io/api/v2/organizations'
8
+
9
+ module Fabricio
10
+ module Authorization
11
+ # A class used for user authorization.
12
+ class AuthorizationClient
13
+
14
+ # Returns a session object for making API requests.
15
+ #
16
+ # @param username [String]
17
+ # @param password [String]
18
+ # @param client_id [String]
19
+ # @param client_secret [String]
20
+ # @return [Fabricio::Authorization::Session]
21
+ def auth(username, password, client_id, client_secret)
22
+ perform_authorization(username, password, client_id, client_secret)
23
+ end
24
+
25
+ # Refreshes an expired session using refresh_token
26
+ #
27
+ # @param session [Fabricio::Authorization::Session] Expired session
28
+ # @return [Fabricio::Authorization::Session]
29
+ def refresh(session)
30
+ perform_refresh_token_request(session)
31
+ end
32
+
33
+ private
34
+
35
+ # Initiates two network requests. One for obtaining session (access and refresh tokens).
36
+ # Another one for obtaining organization identifier.
37
+ #
38
+ # @param username [String]
39
+ # @param password [String]
40
+ # @param client_id [String]
41
+ # @param client_secret [String]
42
+ # @raise [StandardError] Raises error if server sends incorrect response
43
+ # @return [Fabricio::Authorization::Session]
44
+ def perform_authorization(username, password, client_id, client_secret)
45
+ auth_data = obtain_auth_data(username, password, client_id, client_secret)
46
+ if auth_data['access_token'] == nil
47
+ raise StandardError.new("Incorrect authorization response: #{auth_data}")
48
+ end
49
+ organization_id = obtain_organization_id(auth_data)
50
+ Session.new(auth_data, organization_id)
51
+ end
52
+
53
+ # Initiates a session refresh network request
54
+ #
55
+ # @param session [Fabricio::Authorization::Session] Expired session
56
+ # @raise [StandardError] Raises error if server sends incorrect response
57
+ # @return [Fabricio::Authorization::Session]
58
+ def perform_refresh_token_request(session)
59
+ conn = Faraday.new(:url => AUTH_API_URL) do |faraday|
60
+ faraday.adapter Faraday.default_adapter
61
+ end
62
+
63
+ response = conn.post do |req|
64
+ req.headers['Content-Type'] = 'application/json'
65
+ req.body = {
66
+ 'grant_type' => 'refresh_token',
67
+ 'refresh_token' => session.refresh_token
68
+ }.to_json
69
+ end
70
+ result = JSON.parse(response.body)
71
+ if result['access_token'] == nil
72
+ raise StandardError.new("Incorrect authorization response: #{auth_data}")
73
+ end
74
+ Session.new(result, session.organization_id)
75
+ end
76
+
77
+ # Makes a request to OAuth API and obtains access and refresh tokens.
78
+ #
79
+ # @param username [String]
80
+ # @param password [String]
81
+ # @param client_id [String]
82
+ # @param client_secret [String]
83
+ # @return [Hash]
84
+ def obtain_auth_data(username, password, client_id, client_secret)
85
+ conn = Faraday.new(:url => AUTH_API_URL) do |faraday|
86
+ faraday.adapter Faraday.default_adapter
87
+ end
88
+
89
+ response = conn.post do |req|
90
+ req.headers['Content-Type'] = 'application/json'
91
+ req.body = {
92
+ 'grant_type' => 'password',
93
+ 'scope' => 'organizations apps issues features account twitter_client_apps beta software answers',
94
+ 'username' => username,
95
+ 'password' => password,
96
+ 'client_id' => client_id,
97
+ 'client_secret' => client_secret
98
+ }.to_json
99
+ end
100
+ JSON.parse(response.body)
101
+ end
102
+
103
+ # Makes a request to fetch current organization identifier.
104
+ # This identifier is used in most other API requests, so we store it in the session.
105
+ #
106
+ # @param auth_data [Hash] A set of authorization tokens
107
+ # @option options [String] :access_token OAuth access token
108
+ # @option options [String] :refresh_token OAuth refresh token
109
+ # @return [String]
110
+ def obtain_organization_id(auth_data)
111
+ conn = Faraday.new(:url => ORGANIZATION_API_URL) do |faraday|
112
+ faraday.adapter Faraday.default_adapter
113
+ end
114
+
115
+ response = conn.get do |req|
116
+ req.headers['Authorization'] = "Bearer #{auth_data['access_token']}"
117
+ end
118
+ JSON.parse(response.body).first['id']
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,35 @@
1
+ require 'fabricio/authorization/abstract_session_storage'
2
+
3
+ module Fabricio
4
+ module Authorization
5
+ # The only one built-in session storage. Stores current session in memory.
6
+ class MemorySessionStorage < AbstractSessionStorage
7
+
8
+ # Initializes a new MemorySessionStorage object
9
+ #
10
+ # @return [Fabricio::Authorization::MemorySessionStorage]
11
+ def initialize
12
+ @session = nil
13
+ end
14
+
15
+ # Returns session stored in a variable
16
+ #
17
+ # @return [Fabricio::Authorization::Session]
18
+ def obtain_session
19
+ @session
20
+ end
21
+
22
+ # Stores session in a variable
23
+ #
24
+ # @param session [Fabricio::Authorization::MemorySessionStorage]
25
+ def store_session(session)
26
+ @session = session
27
+ end
28
+
29
+ # Resets current state and deletes saved session
30
+ def reset
31
+ @session = nil
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,21 @@
1
+ module Fabricio
2
+ module Authorization
3
+ # This class is a data structure that holds tokens and identifiers necessary for making API requests.
4
+ class Session
5
+ attr_reader :access_token, :refresh_token, :organization_id
6
+
7
+ # Initializes a new Session object
8
+ #
9
+ # @param attributes [Hash] Hash containing access and refresh tokens
10
+ # @option options [String] :access_token OAuth access token
11
+ # @option options [String] :refresh_token OAuth refresh token
12
+ # @param organization_id [String]
13
+ # @return [Fabricio::Authorization::Session]
14
+ def initialize(attributes = {}, organization_id = '')
15
+ @access_token = attributes['access_token']
16
+ @refresh_token = attributes['refresh_token']
17
+ @organization_id = organization_id
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,92 @@
1
+ require 'fabricio/models/organization'
2
+ require 'fabricio/services/organization_service'
3
+ require 'fabricio/services/app_service'
4
+ require 'fabricio/services/build_service'
5
+ require 'fabricio/authorization/authorization_client'
6
+ require 'fabricio/authorization/session'
7
+ require 'fabricio/authorization/memory_session_storage'
8
+ require 'fabricio/networking/network_client'
9
+
10
+ module Fabricio
11
+ # The main object of the gem. It's used to initiate all data requests.
12
+ class Client
13
+ # Default values for initialization parameters
14
+ # clientId and clientSecret are taken from Android application.
15
+ DEFAULT_CLIENT_ID = '2c18f8a77609ee6bbac9e53f3768fedc45fb96be0dbcb41defa706dc57d9c931'
16
+ DEFAULT_CLIENT_SECRET = '092ed1cdde336647b13d44178932cba10911577faf0eda894896188a7d900cc9'
17
+ DEFAULT_USERNAME = nil
18
+ DEFAULT_PASSWORD = nil
19
+ # In-memory session storage is used by default
20
+ DEFAULT_SESSION_STORAGE = Fabricio::Authorization::MemorySessionStorage.new
21
+
22
+ attr_accessor :client_id, :client_secret, :username, :password, :session_storage;
23
+
24
+ # Initializes a new Client object. You can use a block to fill all the options:
25
+ # client = Fabricio::Client.new do |config|
26
+ # config.username = 'email@rambler.ru'
27
+ # config.password = 'pa$$word'
28
+ # end
29
+ #
30
+ # After initializing you can query this client to get data:
31
+ # client.app.all
32
+ # client.app.active_now('app_id')
33
+ #
34
+ # @param options [Hash] Hash containing customizable options
35
+ # @option options [String] :client_id Client identifier. You can take it from the 'Organization' section in Fabric.io settings.
36
+ # @option options [String] :client_secret Client secret key. You can take it from the 'Organization' section in Fabric.io settings.
37
+ # @option options [String] :username Your Fabric.io username
38
+ # @option options [String] :password Your Fabric.io password
39
+ # @option options [Fabricio::Authorization::AbstractSessionStorage] :session_storage Your custom AbstractSessionStorage subclass that provides its own logic of storing session data.
40
+ # @return [Fabricio::Client]
41
+ def initialize(options =
42
+ {
43
+ :client_id => DEFAULT_CLIENT_ID,
44
+ :client_secret => DEFAULT_CLIENT_SECRET,
45
+ :username => DEFAULT_USERNAME,
46
+ :password => DEFAULT_PASSWORD,
47
+ :session_storage => DEFAULT_SESSION_STORAGE
48
+ })
49
+ options.each do |key, value|
50
+ instance_variable_set("@#{key}", value)
51
+ end
52
+ yield(self) if block_given?
53
+
54
+ @auth_client = Fabricio::Authorization::AuthorizationClient.new
55
+ session = obtain_session
56
+ network_client = Fabricio::Networking::NetworkClient.new(@auth_client, @session_storage)
57
+
58
+ @organization_service ||= Fabricio::Service::OrganizationService.new(session, network_client)
59
+ @app_service ||= Fabricio::Service::AppService.new(session, network_client)
60
+ @build_service ||= Fabricio::Service::BuildService.new(session, network_client)
61
+ end
62
+
63
+ # We use `method_missing` approach instead of explicit methods.
64
+ # Generally, look at the initialized services and use the first part of their names as a method.
65
+ # app_service -> client.app
66
+ #
67
+ # # @raise [NoMethodError] Error raised when unsupported method is called.
68
+ def method_missing(*args)
69
+ service = instance_variable_get("@#{args.first}_service")
70
+ return service if service
71
+ raise NoMethodError.new("There's no method called #{args.first} here -- please try again.", args.first)
72
+ end
73
+
74
+ private
75
+
76
+ # Obtains current session. If there is no cached session, it sends a request to OAuth API to get access and refresh tokens.
77
+ #
78
+ # @return [Fabricio::Authorization::Session]
79
+ def obtain_session
80
+ session = @session_storage.obtain_session
81
+ if !session
82
+ session = @auth_client.auth(@username,
83
+ @password,
84
+ @client_id,
85
+ @client_secret)
86
+ @session_storage.store_session(session)
87
+ end
88
+
89
+ session
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,17 @@
1
+ module Fabricio
2
+ module Model
3
+ # Defines a base class for all data models
4
+ class AbstractModel
5
+ # Plain data from the server
6
+ attr_accessor :json
7
+
8
+ # We use `method_missing` approach here to allow a user query any field from the original data structure sent by server.
9
+ def method_missing(*args)
10
+ method_name = args.first
11
+ json_value = @json[method_name.to_s]
12
+ return json_value if json_value
13
+ raise NoMethodError.new("There's no method called #{args.first} here -- please try again.", args.first)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ require 'fabricio/models/abstract_model'
2
+
3
+ module Fabricio
4
+ module Model
5
+ # This model represents an application
6
+ class App < AbstractModel
7
+ attr_reader :id, :name, :bundle_id, :created_at, :platform, :icon_url
8
+
9
+ # Returns an App model object
10
+ #
11
+ # @param attributes [Hash]
12
+ # @return [Fabricio::Model::App]
13
+ def initialize(attributes)
14
+ @id = attributes['id']
15
+ @name = attributes['name']
16
+ @bundle_id = attributes['bundle_identifier']
17
+ @created_at = attributes['created_at']
18
+ @platform = attributes['platform']
19
+ @icon_url = attributes['icon_url']
20
+ @json = attributes
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ require 'fabricio/models/abstract_model'
2
+
3
+ module Fabricio
4
+ module Model
5
+ # This model represents an application build
6
+ class Build < AbstractModel
7
+ attr_reader :id, :version, :build_number, :release_notes, :distributed_at
8
+
9
+ # Returns a Build model object
10
+ #
11
+ # @param attributes [Hash]
12
+ # @return [Fabricio::Model::Build]
13
+ def initialize(attributes)
14
+ @id = attributes['id']
15
+ @version = attributes['build_version']['display_version']
16
+ @build_number = attributes['build_version']['build_version']
17
+ @release_notes = attributes['release_notes_summary']
18
+ @distributed_at = attributes['distributed_at']
19
+ @json = attributes
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ require 'fabricio/models/abstract_model'
2
+
3
+ module Fabricio
4
+ module Model
5
+ # This model represents an organization
6
+ class Organization < AbstractModel
7
+ attr_reader :id, :alias, :name, :apps_counts
8
+
9
+ # Returns a Build model object
10
+ #
11
+ # @param attributes [Hash]
12
+ # @return [Fabricio::Model::Organization]
13
+ def initialize(attributes)
14
+ @id = attributes['id']
15
+ @alias = attributes['alias']
16
+ @name = attributes['name']
17
+ @apps_counts = attributes['apps_counts']
18
+ @json = attributes
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Fabricio
2
+ module Model
3
+ # This model represents a data point with two fields - timestamp and value
4
+ class Point
5
+ attr_reader :date, :value
6
+
7
+ # Returns a data point with two fields - timestamp and value
8
+ #
9
+ # @param attributes [Hash]
10
+ # @return [Fabricio::Model::Point]
11
+ def initialize(attributes)
12
+ @date = DateTime.strptime(attributes.first.to_s,'%s')
13
+ @value = attributes.last
14
+ end
15
+ end
16
+ end
17
+ end