rollout_service 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f4eb23b9f1860c997270a8133701f70fbf8dc7ec
4
+ data.tar.gz: 9998ef0936cc0b713ca92d44c78ae4b0bf57624b
5
+ SHA512:
6
+ metadata.gz: 95b8179d34b583a77879ec2dd229522e2eea0d4bfac04b63ad8dc514577b9ba9432370a83a6bc6077a3c29da5f6cd64503fd4af9304d19563a40cb15a303f599
7
+ data.tar.gz: fab775be9f45b368a6f7cf0fb632295dd12200641b6feacca2ad5494c5ebd971f864dd9a2b13a54973bd2919d7f2965de1e106534a8001a3cdc632411b15dc8c
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .idea/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in testttt.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,79 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rollout_service (0.1.0)
5
+ active_attr (~> 0.9.0)
6
+ activesupport (~> 5.0)
7
+ grape
8
+ grape-entity (~> 0.5.0)
9
+ redis
10
+ require_all
11
+ rollout
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ active_attr (0.9.0)
17
+ activemodel (>= 3.0.2, < 5.1)
18
+ activesupport (>= 3.0.2, < 5.1)
19
+ activemodel (5.0.7)
20
+ activesupport (= 5.0.7)
21
+ activesupport (5.0.7)
22
+ concurrent-ruby (~> 1.0, >= 1.0.2)
23
+ i18n (>= 0.7, < 2)
24
+ minitest (~> 5.1)
25
+ tzinfo (~> 1.1)
26
+ axiom-types (0.1.1)
27
+ descendants_tracker (~> 0.0.4)
28
+ ice_nine (~> 0.11.0)
29
+ thread_safe (~> 0.3, >= 0.3.1)
30
+ builder (3.2.3)
31
+ coercible (1.0.0)
32
+ descendants_tracker (~> 0.0.1)
33
+ concurrent-ruby (1.0.5)
34
+ descendants_tracker (0.0.4)
35
+ thread_safe (~> 0.3, >= 0.3.1)
36
+ equalizer (0.0.11)
37
+ grape (1.0.3)
38
+ activesupport
39
+ builder
40
+ mustermann-grape (~> 1.0.0)
41
+ rack (>= 1.3.0)
42
+ rack-accept
43
+ virtus (>= 1.0.0)
44
+ grape-entity (0.5.2)
45
+ multi_json (>= 1.3.2)
46
+ i18n (1.0.1)
47
+ concurrent-ruby (~> 1.0)
48
+ ice_nine (0.11.2)
49
+ minitest (5.11.3)
50
+ multi_json (1.13.1)
51
+ mustermann (1.0.2)
52
+ mustermann-grape (1.0.0)
53
+ mustermann (~> 1.0.0)
54
+ rack (2.0.5)
55
+ rack-accept (0.4.5)
56
+ rack (>= 0.4)
57
+ rake (10.5.0)
58
+ redis (4.0.1)
59
+ require_all (2.0.0)
60
+ rollout (2.4.3)
61
+ thread_safe (0.3.6)
62
+ tzinfo (1.2.5)
63
+ thread_safe (~> 0.1)
64
+ virtus (1.0.5)
65
+ axiom-types (~> 0.1)
66
+ coercible (~> 1.0)
67
+ descendants_tracker (~> 0.0, >= 0.0.3)
68
+ equalizer (~> 0.0, >= 0.0.9)
69
+
70
+ PLATFORMS
71
+ ruby
72
+
73
+ DEPENDENCIES
74
+ bundler (~> 1.13)
75
+ rake (~> 10.0)
76
+ rollout_service!
77
+
78
+ BUNDLED WITH
79
+ 1.16.2
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Rollout-Service
2
+ **A Grape service that expose rollout gem via RESTful endpoints**
3
+
4
+ This service expose RESTfull endpoints that allows you to perform CRUD operation on [rollout](https://github.com/fetlife/rollout) gem.
5
+
6
+ This service works great with [Rollout-Dashboard](https://github.com/fiverr/rollout_dashboard) - a beautiful user interface for rollout gem)
7
+
8
+ ## End-Points Documentation:
9
+
10
+ | Description | END POINT |
11
+ | ------------- | ------------- |
12
+ | Get all features | GET /api/v1/features |
13
+ | Get specific feature by name | GET /api/v1/features/:feature_name |
14
+ | Get specific feature by name | GET /api/v1/features/:feature_name |
15
+ | Check if feature is active | GET /api/v1/features/:feature_name/:user_id/active |
16
+ | Create a new feature | POST /api/v1/features/:feature_name |
17
+ | Partially update existing feature | PATCH /api/v1/features/:feature_name |
18
+ | Delete a feature | DELETE /api/v1/features/:feature_name |
19
+
20
+
21
+ # FAQ
22
+
23
+ # How to set redis configuration?
24
+ Edit `./config/redis.yml`
25
+
26
+ ## How to start the service?
27
+ run `bundle exec rackup -p :port`
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
@@ -0,0 +1,119 @@
1
+ module RolloutService
2
+ module API
3
+ class Features < Grape::API
4
+
5
+ get '/' do
6
+ features = Config.rollout.features
7
+ features.map! do|feature|
8
+ Models::Feature.find(feature)
9
+ end
10
+
11
+ RestfulModels::Response.represent(data: features)
12
+ end
13
+
14
+
15
+ route_param :feature_name do
16
+ params do
17
+ requires :feature_name, type: Models::Feature
18
+ end
19
+ get '/' do
20
+ feature = params[:feature_name]
21
+
22
+ if feature.valid?
23
+ RestfulModels::Response.represent(data: feature)
24
+ else
25
+ status 500
26
+ RestfulModels::Response.represent(message: 'Error, feature is not valid')
27
+ end
28
+ end
29
+
30
+ params do
31
+ requires :user_id, type: Integer, desc: 'The user ID'
32
+ requires :feature_name, type: Models::Feature
33
+ end
34
+ get '/:user_id/active' do
35
+ user_id = params[:user_id].to_i
36
+ feature = params[:feature_name]
37
+
38
+ is_active = feature.active?(user_id)
39
+
40
+ RestfulModels::Response.represent(data: { active: is_active })
41
+ end
42
+
43
+ params do
44
+ requires :feature_name, type: Models::Feature
45
+ end
46
+ delete '/' do
47
+ feature = params[:feature_name]
48
+ feature.delete
49
+ ''
50
+ end
51
+
52
+ params do
53
+ requires :description, type: String, desc: 'The feature description'
54
+ requires :feature_name, type: String
55
+ end
56
+ post '/' do
57
+ feature_name = params[:feature_name]
58
+ error! 'Feature is already exist!' if Models::Feature.exist?(feature_name)
59
+
60
+ options = {
61
+ name: feature_name,
62
+ percentage: params[:percentage].to_i,
63
+ description: params[:description],
64
+ author: current_user.name,
65
+ author_mail: current_user.email,
66
+ created_at: Time.current
67
+ }
68
+
69
+ feature = Models::Feature.new(options)
70
+
71
+ begin
72
+ feature.save!
73
+ Models::Feature.set_users_to_feature(feature, params[:users])
74
+ RestfulModels::Response.represent(
75
+ message: 'Feature created successfully!',
76
+ data: feature
77
+ )
78
+ rescue => e
79
+ status 500
80
+ RestfulModels::Response.represent(message: "An error has been occurred.\r\n #{e}")
81
+ end
82
+ end
83
+
84
+ params do
85
+ requires :feature_name, type: Models::Feature
86
+ end
87
+ patch '/' do
88
+ feature = params[:feature_name]
89
+
90
+ options = {
91
+ percentage: params[:percentage].to_i,
92
+ description: params[:description],
93
+ created_at: Time.current
94
+ }
95
+
96
+ # if the feature for some reason had no author, the current user become
97
+ if feature.author.blank?
98
+ options[:author] = current_user.name
99
+ options[:author_mail] = current_user.email
100
+ end
101
+
102
+ feature.assign_attributes(options)
103
+
104
+ begin
105
+ feature.save!
106
+ Models::Feature.set_users_to_feature(feature, params[:users])
107
+ RestfulModels::Response.represent(
108
+ message: 'Feature updated successfully!',
109
+ data: feature
110
+ )
111
+ rescue => e
112
+ status 500
113
+ RestfulModels::Response.represent(message: "An error has been occurred.\r\n #{e}")
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,14 @@
1
+ module RolloutService
2
+ module Config
3
+ extend self
4
+ attr_accessor :rollout, :redis
5
+
6
+ def configure
7
+ yield self
8
+ raise 'You must provide a redis instance' if redis.blank?
9
+
10
+ self.rollout = Rollout.new(redis, use_sets: true)
11
+ end
12
+ end
13
+ end
14
+
@@ -0,0 +1,107 @@
1
+ module RolloutService
2
+ module Models
3
+ class Feature
4
+ include ActiveAttr::Model
5
+
6
+ MAX_HISTORY_RECORDS = 50
7
+
8
+ attribute :name, type: String
9
+ attribute :description, type: String
10
+ attribute :percentage, type: Integer, default: 0
11
+ attribute :author, type: String
12
+ attribute :author_mail, type: String
13
+ attribute :created_at, type: Date
14
+ attribute :history, default: []
15
+ attribute :users, default: []
16
+
17
+ validates :name,
18
+ :description,
19
+ :percentage,
20
+ :created_at,
21
+ :author,
22
+ :author_mail,
23
+ presence: true
24
+
25
+ def self.parse(name)
26
+ instance = find(name)
27
+ raise 'Feature is not exist' if instance.nil?
28
+ instance
29
+ end
30
+
31
+ def self.find(name)
32
+ return nil unless exist?(name)
33
+
34
+ feature = Config::rollout.get(name)
35
+ feature_data = feature.data.deep_symbolize_keys!
36
+
37
+ feature_data.merge!({
38
+ name: feature.name,
39
+ percentage: feature.percentage,
40
+ users: feature.users
41
+ })
42
+
43
+ feature_data.delete_if {|key, _| !self.method_defined?(key)}
44
+
45
+ self.new(feature_data)
46
+ end
47
+
48
+ def self.exist?(name)
49
+ features = Config::rollout.features
50
+ features.include?(name.to_sym)
51
+ end
52
+
53
+ def self.set_users_to_feature(rollout, users)
54
+ return if users.nil? || rollout.nil?
55
+ users = users.to_a
56
+
57
+ current_active_users = rollout.users
58
+ users_to_remove = current_active_users - users
59
+ Config::rollout.deactivate_users(rollout.name ,users_to_remove)
60
+ Config::rollout.activate_users(rollout.name ,users)
61
+ rollout.users = users
62
+ users
63
+ end
64
+
65
+ def save!
66
+ set_history_attribute
67
+ raise 'Feature is not valid!' unless self.valid?
68
+
69
+ Config::rollout.activate_percentage(self.name, self.percentage)
70
+
71
+ feature_data = {
72
+ history: self.history,
73
+ description: self.description,
74
+ author: self.author,
75
+ author_mail: self.author_mail,
76
+ created_at: self.created_at
77
+ }
78
+
79
+ feature_data.delete_if { |_, value| value.blank? }
80
+ Config::rollout.set_feature_data(self.name, feature_data)
81
+ end
82
+
83
+ def active?(user_id)
84
+ Config::rollout.active?(self.name, user_id)
85
+ end
86
+
87
+ def delete
88
+ Config::rollout.delete(self.name)
89
+ end
90
+
91
+ private
92
+
93
+ def set_history_attribute
94
+ last_record = history.last
95
+ return if last_record.present? && last_record[:percentage] == self.percentage
96
+
97
+ self.history << {
98
+ author: self.author,
99
+ author_mail: self.author_mail,
100
+ percentage: self.percentage,
101
+ updated_at: Time.current
102
+ }
103
+ self.history = self.history.last(MAX_HISTORY_RECORDS)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,12 @@
1
+ module RolloutService
2
+ class User
3
+ attr_reader :name, :email
4
+
5
+ def initialize(env)
6
+ user = env['User-Details'] || ''
7
+ name, email = user.split(':')
8
+ @name = name || 'Anonymous'
9
+ @email = email || 'anonymous@anonymous.con'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ module RolloutService
2
+ module RestfulModels
3
+ class Response < Grape::Entity
4
+ expose :data, unless: Proc.new {|field| field.blank?}
5
+ expose :message, if: ->(response, _) { response[:data].blank? }
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ module RolloutService
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,27 @@
1
+ require 'rollout_service/version'
2
+ require 'require_all'
3
+ require 'grape'
4
+ require 'grape-entity'
5
+ require 'rollout'
6
+ require 'redis'
7
+ require 'active_support'
8
+ require 'active_attr'
9
+
10
+ require_rel 'rollout_service/config'
11
+ require_rel 'rollout_service/restful_models'
12
+ require_rel 'rollout_service/models'
13
+ require_rel 'rollout_service/api'
14
+
15
+ module RolloutService
16
+ class Service < Grape::API
17
+ format :json
18
+
19
+ helpers do
20
+ def current_user
21
+ @current_user ||= User.new(env)
22
+ end
23
+ end
24
+
25
+ resource(:features) { mount API::Features }
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rollout_service/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rollout_service"
8
+ spec.version = RolloutService::VERSION
9
+ spec.authors = ["Fiverr"]
10
+ spec.email = ["dev@fiverr.com"]
11
+ spec.summary = "This gem exposes rollout gem API"
12
+ spec.homepage = "https://www.fiverr.com"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
15
+ f.match(%r{^(test|spec|features)/})
16
+ end
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_dependency 'grape', '~> 1'
20
+ spec.add_dependency 'grape-entity', '~> 0.5'
21
+ spec.add_dependency 'rollout', '~> 2.4'
22
+ spec.add_dependency 'require_all', '~> 2'
23
+ spec.add_dependency 'activesupport', '~> 5'
24
+ spec.add_dependency 'active_attr', '~> 0.9'
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.13"
27
+ spec.add_development_dependency "rake", "~> 10.0"
28
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rollout_service
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Fiverr
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: grape
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: grape-entity
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rollout
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.4'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: require_all
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '5'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: active_attr
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.9'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.13'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.13'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '10.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '10.0'
125
+ description:
126
+ email:
127
+ - dev@fiverr.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - Gemfile
134
+ - Gemfile.lock
135
+ - README.md
136
+ - Rakefile
137
+ - lib/rollout_service.rb
138
+ - lib/rollout_service/api/feature_api.rb
139
+ - lib/rollout_service/config/config.rb
140
+ - lib/rollout_service/models/feature.rb
141
+ - lib/rollout_service/models/user.rb
142
+ - lib/rollout_service/restful_models/response.rb
143
+ - lib/rollout_service/version.rb
144
+ - rollout_service.gemspec
145
+ homepage: https://www.fiverr.com
146
+ licenses: []
147
+ metadata: {}
148
+ post_install_message:
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubyforge_project:
164
+ rubygems_version: 2.5.2
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: This gem exposes rollout gem API
168
+ test_files: []