performify 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e5d73a6fd3521ef86fbaeae9cb733e410e6101a5
4
- data.tar.gz: fd8f33d33a3d9ac5309b53c097c1417db447c5cb
3
+ metadata.gz: c4e16b9f54a462104c0d2851bbbb75d30a36ae4d
4
+ data.tar.gz: 4788ceeb7f37d2210c400afdbc628497386d2d9a
5
5
  SHA512:
6
- metadata.gz: da1783feeb0b3a51ed5314cb045e00912d67622f4d29231b532b4a49f0bfc25b8f4c7fdfcbc588bb85c0bcd484dd093746feb96943ce534c52ec791661245a6b
7
- data.tar.gz: 7e2ac421eff8be0222d97263266e784ad281e02e780c5c54aaf66151e0252b7c66274794ff121b0e8f82130c2e4169aeb3e8c0e43006ebb69997e754dfd15fe5
6
+ metadata.gz: a08337228b009943e7995b4384c30724f7685264bcea0ba1afda47643e5790924a8b84f6971cb4be0a5d071e882096b4516595a80b8bee10c78f6e5db4a7d7ec
7
+ data.tar.gz: 2516c4e62c09d77ed0681a46c06ca2650f906d480a57f61bd4f5091e02bf0753a8005622c6dcad49a8cfba20b6d76de3ce30cd1063f2d2694bfed3767d3b30cc
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .byebug_history
data/.rubocop.yml ADDED
@@ -0,0 +1,81 @@
1
+ AllCops:
2
+ Exclude:
3
+ - 'bin/*'
4
+ - 'spec/**/*_spec.rb'
5
+
6
+ Rails:
7
+ Enabled: false
8
+
9
+ Documentation:
10
+ Enabled: false
11
+
12
+ Metrics/ClassLength:
13
+ CountComments: false
14
+ Max: 300
15
+
16
+ Metrics/LineLength:
17
+ Max: 120
18
+ AllowHeredoc: true
19
+ AllowURI: true
20
+ URISchemes:
21
+ - http
22
+ - https
23
+
24
+ Style/ClassAndModuleChildren:
25
+ Enabled: false
26
+
27
+ Rails/Validation:
28
+ Enabled: false
29
+
30
+ Rails/HasAndBelongsToMany:
31
+ Enabled: false
32
+
33
+ Metrics/MethodLength:
34
+ Max: 30
35
+ Exclude:
36
+ - db/migrate/*
37
+
38
+ Lint/HandleExceptions:
39
+ Enabled: false
40
+
41
+ Metrics/AbcSize:
42
+ Enabled: false
43
+
44
+ Style/EmptyLinesAroundClassBody:
45
+ Enabled: false
46
+
47
+ Style/EmptyLinesAroundBlockBody:
48
+ Enabled: false
49
+
50
+ Style/CommentAnnotation:
51
+ Enabled: false
52
+
53
+ Style/FrozenStringLiteralComment:
54
+ Enabled: false
55
+
56
+ Style/StringLiterals:
57
+ Enabled: false
58
+
59
+ Style/SpaceInsideBrackets:
60
+ Enabled: false
61
+
62
+ Style/NumericLiterals:
63
+ Enabled: false
64
+
65
+ Style/DoubleNegation:
66
+ Enabled: false
67
+
68
+ Style/MultilineMethodCallIndentation:
69
+ Enabled: false
70
+
71
+ Style/ClosingParenthesisIndentation:
72
+ Enabled: false
73
+
74
+ Style/MultilineMethodCallBraceLayout:
75
+ Enabled: false
76
+
77
+ Style/IfUnlessModifier:
78
+ Enabled: false
79
+
80
+ Bundler/OrderedGems:
81
+ Enabled: false
data/.travis.yml CHANGED
@@ -2,4 +2,9 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - 2.3.1
5
- before_install: gem install bundler -v 1.14.6
5
+ before_install:
6
+ - gem install bundler -v 1.14.6
7
+ install:
8
+ - bin/setup
9
+ before_script:
10
+ - bin/rubocop
data/README.md CHANGED
@@ -1,8 +1,6 @@
1
1
  # Performify
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/performify`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ It's well-known practice that has been proved in many large projects to move server logic into separated service classes. This approach gives a lot of advantages, because when you are able to create object that incapsulates your logic it's much easier to develop, search, control and test. And `performify` helps you to do it in nice and easy way with minimum of pain and maximum of result.
6
4
 
7
5
  ## Installation
8
6
 
@@ -14,28 +12,246 @@ gem 'performify'
14
12
 
15
13
  And then execute:
16
14
 
17
- $ bundle
15
+ ```
16
+ $ bundle
17
+ ```
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install performify
21
+ ```
22
+ $ gem install performify
23
+ ```
22
24
 
23
25
  ## Usage
24
26
 
25
- TODO: Write usage instructions here
27
+ How to have a deal with services:
28
+
29
+ 1. Define `ApplicationService`
30
+ 2. Create new service inherited from `ApplicationService`
31
+ 3. Implement `execute!` method
32
+ 4. Use `super` to work with db transaction and automatic success / fail workflow control
33
+ 5. Use `success!` and `fail!` to control everything by hands
34
+ 6. Use `on_success` / `on_fail` to define callbacks
35
+
36
+ ### ApplicationService
37
+
38
+ So, first of all it's better to create `ApplicationService` class that will be used as base for all services in your project. You can put any shared logic (like, authorization, for example) here:
39
+
40
+ ```ruby
41
+ class ApplicationService < Performify::Base
42
+ def authorize!(record)
43
+ # you can put authorization logic here and use it from inherited services
44
+ end
45
+ end
46
+ ```
47
+
48
+ This is, for example, how authorization can be implemented for `Pundit`:
49
+
50
+ ```ruby
51
+ class ApplicationService < Performify::Base
52
+ def authorize!(record, query = default_query, record_policy = nil)
53
+ record_policy ||= policy(record)
54
+ return if record_policy.public_send(query)
55
+
56
+ raise Pundit::NotAuthorizedError, query: query, record: record, policy: record_policy
57
+ end
58
+
59
+ def default_query
60
+ @default_query ||= "#{self.class.name.demodulize.underscore.to_sym}?"
61
+ end
62
+
63
+ def policy(record)
64
+ @policy ||= Pundit.policy!(@current_user, record)
65
+ end
66
+ end
67
+ ```
68
+
69
+ ### Service: database
70
+
71
+ Now, to define new service just create new class and inherit it from `ApplicationService`:
72
+
73
+ ```ruby
74
+ module Users
75
+ class Destroy < ApplicationService
76
+ def execute!
77
+ # current user is already available, so feel free to use it
78
+ # to get user's context
79
+
80
+ authorize! current_user unless force?
81
+
82
+ # block passed into super's implementation will be executed
83
+ # in transaction, so you can do multiple data operations, and final
84
+ # result of this block will be used to determine result of execution
85
+
86
+ super do
87
+ if current_user.update(destroyed_at: Time.zone.now)
88
+ current_user.comments.find_each do |c|
89
+ s = Comments::Destroy.new(current_user, c)
90
+ s.execute!
91
+
92
+ # it's also ok to raise ActiveRecord::Rollback, it will be handled
93
+ # gracefully as regular execution fail
94
+
95
+ raise ActiveRecord::Rollback unless s.success?
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ def force?
102
+ # additional instance variables can be passed as named args into
103
+ # initializer and accessed in service flow
104
+
105
+ force.present?
106
+ end
107
+ end
108
+ end
109
+ ```
110
+
111
+ Now you can create instance of your service and check result of execution:
112
+
113
+ ```ruby
114
+ service = Users::Destroy.new(current_user, force: true)
115
+ service.execute!
116
+ service.success? # or service.fail?
117
+ ```
118
+
119
+ ### Service: HTTP API
120
+
121
+ Sometimes your service doesn't work with database, but calls some http endpoint or do some other stuff that doesn't require db transaction. In this case you can control your service flow manually:
122
+
123
+ ```ruby
124
+ class Stripe::Create < ApplicationService
125
+ attr_reader :subscription
126
+
127
+ def execute!
128
+ # here you can go to Stripe and create subscription for the user
129
+ begin
130
+ @subscription = Stripe::Subscription.create(
131
+ customer: current_user.customer_id,
132
+ plan: selected_plan.stripe_name,
133
+ )
134
+
135
+ # everything looks ok, success
136
+
137
+ success!
138
+ rescue Stripe::StripeError => e
139
+ # something went wrong, let's notify developers and say that
140
+ # service execution has been failed
141
+
142
+ Airbrake.notify(e)
143
+ fail!
144
+ end
145
+ end
146
+ end
147
+ ```
148
+
149
+ ### Callbacks
150
+
151
+ If you need to do something on service success / fail it is possible to define appropriate callbacks. Notice, that in case of using `super` callbacks will be executed outside of db transaction, so it's safe to send emails from there, for example.
152
+
153
+ ```ruby
154
+ module Passwords
155
+ class Update < ApplicationService
156
+ def execute!
157
+ authorize! current_user
158
+
159
+ super do
160
+ current_user.update(password: password, password_confirmation: password_confirmation)
161
+ end
162
+ end
163
+
164
+ # you can pass method name as a callback
165
+
166
+ on_success :invalidate_sessions
167
+
168
+ # or you can pass block instead of method name
169
+
170
+ on_success { UserMailer.password_changed(current_user).deliver_later }
171
+
172
+ private def invalidate_sessions
173
+ # you can invalidate existing user's sessions here
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### Validation
180
+
181
+ Performify allows you to validate input arguments using [dry-validation](http://dry-rb.org/gems/dry-validation/) schemas. Validation is performed on creation of service instance. And if validation is not passed it will be impossible to call execution. Result of execution will be automatically switched to failed state.
182
+
183
+ ```ruby
184
+ module Users
185
+ module Create
186
+ schema do
187
+ required(:email).filled(:str?)
188
+ end
189
+
190
+ def execute!
191
+ # it will be impossible to call execution if provided arguments
192
+ # did not pass validation
193
+ end
194
+ end
195
+ end
196
+
197
+ service = Users::Create.new(current_user, email: nil)
198
+ service.execute! # nothing happens here
199
+ service.success? # will be false because of validation
200
+ service.errors # contains hash of errors
201
+ ```
202
+
203
+ Sometimes you can have differences between validation errors and execution errors. But usually it's boring to check them separately since you just need to display final result to user. To avoid double check you can use following trick:
204
+
205
+ ```ruby
206
+ module Users
207
+ module Create
208
+ attr_reader :user
209
+
210
+ schema do
211
+ required(:email).filled(:str?)
212
+ end
213
+
214
+ def execute!
215
+ user = User.new(email: email)
216
+ authorize! user
217
+
218
+ # Let's assume that user has additional validation of uniqueness on the
219
+ # level of model, so in controller you need to check separately service's
220
+ # errors and model's errors, right?
221
+
222
+ super { user.save }
223
+ end
224
+
225
+ # So, we define on fail callback where we copy errors from model
226
+ # to service so now in controller we can check and use only service's errors
227
+
228
+ on_fail { errors! user.errors.to_h }
229
+ end
230
+ end
231
+
232
+ # in controller
233
+
234
+ service = Users::Create.new(current_user, email: nil)
235
+ service.execute!
236
+
237
+ if service.success?
238
+ # respond with ok
239
+ else
240
+ # respond with unprocessable entity and service.errors
241
+ end
242
+ ```
26
243
 
27
244
  ## Development
28
245
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
246
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
247
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
248
+ To install this gem onto your local machine, run `bin/rake install`. To release a new version, update the version number in `version.rb`, and then run `bin/rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
249
 
33
250
  ## Contributing
34
251
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/performify.
252
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kimrgrey/performify.
36
253
 
37
254
 
38
255
  ## License
39
256
 
40
257
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
-
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
3
 
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- task :default => :spec
6
+ task default: :spec
data/bin/rubocop ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ #
4
+ # This file was generated by Bundler.
5
+ #
6
+ # The application 'rubocop' is installed as part of a gem, and
7
+ # this file is here to facilitate running it.
8
+ #
9
+
10
+ require "pathname"
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
12
+ Pathname.new(__FILE__).realpath)
13
+
14
+ require "rubygems"
15
+ require "bundler/setup"
16
+
17
+ load Gem.bin_path("rubocop", "rubocop")
@@ -1,20 +1,28 @@
1
1
  require 'performify/callbacks'
2
+ require 'performify/validation'
2
3
 
3
4
  module Performify
4
5
  class Base
5
6
  extend Performify::Callbacks
7
+ extend Performify::Validation
6
8
 
7
9
  attr_reader :current_user
8
10
 
9
11
  def initialize(current_user = nil, **args)
10
12
  @current_user = current_user
11
13
 
12
- args.each do |arg_name, arg_value|
14
+ return if args.empty?
15
+
16
+ validate(args).each do |arg_name, arg_value|
13
17
  define_singleton_method(arg_name) { arg_value }
14
18
  end
19
+
20
+ fail!(with_callbacks: true) if errors?
15
21
  end
16
22
 
17
23
  def execute!
24
+ return if defined?(@result)
25
+
18
26
  block_result = nil
19
27
 
20
28
  ActiveRecord::Base.transaction do
@@ -2,7 +2,7 @@ module Performify
2
2
  module Callbacks
3
3
  class UnknownTypeOfCallbackError < StandardError; end
4
4
 
5
- TYPES_OF_CALLBACK = [ :success, :fail ].freeze
5
+ TYPES_OF_CALLBACK = %i[success fail].freeze
6
6
 
7
7
  def clean_callbacks
8
8
  @service_callbacks = {}
@@ -0,0 +1,42 @@
1
+ require 'dry-validation'
2
+
3
+ module Performify
4
+ module Validation
5
+ def self.extended(base)
6
+ base.extend Performify::Validation::ClassMethods
7
+ base.include Performify::Validation::InstanceMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def schema(&block)
12
+ return @schema unless block_given?
13
+ @schema = Dry::Validation.Schema(Dry::Validation::Schema::Form, {}, &block)
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def schema
19
+ self.class.schema
20
+ end
21
+
22
+ def validate(args)
23
+ return args if schema.nil?
24
+ result = schema.call(args)
25
+ errors!(result.errors) unless result.success?
26
+ result.output
27
+ end
28
+
29
+ def errors!(new_errors)
30
+ errors.merge!(new_errors)
31
+ end
32
+
33
+ def errors
34
+ @errors ||= {}
35
+ end
36
+
37
+ def errors?
38
+ errors.any?
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,3 +1,3 @@
1
1
  module Performify
2
- VERSION = "0.1.0"
2
+ VERSION = '0.2.0'.freeze
3
3
  end
data/performify.gemspec CHANGED
@@ -1,37 +1,37 @@
1
1
  # coding: utf-8
2
+
2
3
  lib = File.expand_path('../lib', __FILE__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'performify/version'
5
6
 
6
7
  Gem::Specification.new do |spec|
7
- spec.name = "performify"
8
- spec.version = Performify::VERSION
9
- spec.authors = ["Sergey Tsvetkov"]
10
- spec.email = ["sergey.a.tsvetkov@gmail.com"]
8
+ spec.name = "performify"
9
+ spec.version = Performify::VERSION
10
+ spec.authors = ["Sergey Tsvetkov"]
11
+ spec.email = ["sergey.a.tsvetkov@gmail.com"]
11
12
 
12
- spec.summary = %q{
13
- Service classes that makes your life easier
14
- }
13
+ spec.summary = "Service classes that makes your life easier"
15
14
 
16
- spec.description = %q{
17
- Performify helps you to define your app logic in separated and isolated service classes
18
- that is easy to use, test and maitain.
19
- }
15
+ spec.description = "Performify helps you to define your app logic in separated and isolated \
16
+ service classes that is easy to use, test and maitain."
20
17
 
21
- spec.homepage = "https://github.com/kimrgrey/performify"
22
- spec.license = "MIT"
18
+ spec.homepage = "https://github.com/kimrgrey/performify"
19
+ spec.license = "MIT"
23
20
 
24
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
21
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
22
  f.match(%r{^(test|spec|features)/})
26
23
  end
27
- spec.bindir = "exe"
28
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
26
  spec.require_paths = ["lib"]
30
27
 
31
28
  spec.add_dependency "activerecord", ">= 4.0.0"
29
+ spec.add_dependency "dry-validation", ">= 0.10.7"
32
30
 
33
- spec.add_development_dependency "sqlite3"
31
+ spec.add_development_dependency "sqlite3", "~> 1.3.13"
34
32
  spec.add_development_dependency "bundler", "~> 1.14"
35
33
  spec.add_development_dependency "rake", "~> 10.0"
36
34
  spec.add_development_dependency "rspec", "~> 3.5"
35
+ spec.add_development_dependency "rubocop", "~> 0.48"
36
+ spec.add_development_dependency "byebug", "~> 9.0.6"
37
37
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: performify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Tsvetkov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-04-29 00:00:00.000000000 Z
11
+ date: 2017-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -25,19 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: 4.0.0
27
27
  - !ruby/object:Gem::Dependency
28
- name: sqlite3
28
+ name: dry-validation
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
33
+ version: 0.10.7
34
+ type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '0'
40
+ version: 0.10.7
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 1.3.13
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 1.3.13
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -80,8 +94,36 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '3.5'
83
- description: "\n Performify helps you to define your app logic in separated and
84
- isolated service classes\n that is easy to use, test and maitain.\n "
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.48'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.48'
111
+ - !ruby/object:Gem::Dependency
112
+ name: byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 9.0.6
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 9.0.6
125
+ description: Performify helps you to define your app logic in separated and isolated service
126
+ classes that is easy to use, test and maitain.
85
127
  email:
86
128
  - sergey.a.tsvetkov@gmail.com
87
129
  executables: []
@@ -90,6 +132,7 @@ extra_rdoc_files: []
90
132
  files:
91
133
  - ".gitignore"
92
134
  - ".rspec"
135
+ - ".rubocop.yml"
93
136
  - ".travis.yml"
94
137
  - Gemfile
95
138
  - LICENSE.txt
@@ -98,10 +141,12 @@ files:
98
141
  - bin/console
99
142
  - bin/rake
100
143
  - bin/rspec
144
+ - bin/rubocop
101
145
  - bin/setup
102
146
  - lib/performify.rb
103
147
  - lib/performify/base.rb
104
148
  - lib/performify/callbacks.rb
149
+ - lib/performify/validation.rb
105
150
  - lib/performify/version.rb
106
151
  - performify.gemspec
107
152
  homepage: https://github.com/kimrgrey/performify