power-types 0.3.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.7
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2016 Ignacio Baixas
3
+ Copyright 2022 Platanus
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Power Types
2
- [![Gem Version](https://badge.fury.io/rb/power-types.svg)](https://badge.fury.io/rb/power-types) [![Build Status](https://travis-ci.org/platanus/power-types.svg?branch=master)](https://travis-ci.org/platanus/power-types) [![Coverage Status](https://coveralls.io/repos/github/platanus/power-types/badge.svg)](https://coveralls.io/github/platanus/power-types)
2
+ [![Gem Version](https://badge.fury.io/rb/power-types.svg)](https://badge.fury.io/rb/power-types)
3
+ [![CircleCI](https://circleci.com/gh/platanus/local_resource.svg?style=shield)](https://app.circleci.com/pipelines/github/platanus/local_resource)
4
+ [![Coverage Status](https://coveralls.io/repos/github/platanus/power-types/badge.svg)](https://coveralls.io/github/platanus/power-types)
3
5
 
4
6
  Rails pattern enforcing types used by the Platanus team.
5
7
 
@@ -8,7 +10,7 @@ Rails pattern enforcing types used by the Platanus team.
8
10
  In Rails projects, Platanus encourages to use classes beyond models and controllers to hold the app's logic.
9
11
  These powerful types proposed are Services, Commands, Observers, Utils and Values.
10
12
 
11
- For a deeper understanding about the usage of these patterns, feel welcome to read the [related post in Platanus Blog](https://blog.platan.us/services-commands-y-otros-poderosos-patrones-en-rails) (in spanish).
13
+ For a deeper understanding about the usage of these patterns, feel welcome to read the [related post in Platanus Blog](https://blog.platan.us/services-commands-y-otros-poderosos-patrones-en-rails-27c2d3aa7c2e) (in spanish).
12
14
 
13
15
  The goal aimed with this gem is to go further, and not just apply this patterns over POROs (plain simple ruby classes). The gem provides an special structure and syntax to create and run services, commands and more, with ease.
14
16
 
@@ -28,10 +30,20 @@ bundle install
28
30
 
29
31
  ## Power types
30
32
 
31
- - [Services](#services)
32
- - [Commands](#commands)
33
- - [Observers](#observers)
34
- - [Values and Utils](#values-and-utils)
33
+ - [Power Types](#power-types)
34
+ - [Introduction](#introduction)
35
+ - [Installation](#installation)
36
+ - [Power types](#power-types-1)
37
+ - [Services](#services)
38
+ - [Commands](#commands)
39
+ - [Observers](#observers)
40
+ - [Values](#values)
41
+ - [Presenters](#presenters)
42
+ - [Utils](#utils)
43
+ - [Publishing](#publishing)
44
+ - [Contributing](#contributing)
45
+ - [Credits](#credits)
46
+ - [License](#license)
35
47
 
36
48
  ### Services
37
49
 
@@ -201,7 +213,7 @@ describe MyModelObserver do
201
213
  end
202
214
  ```
203
215
 
204
- Now, suppose you have defined the following model (with name and villian attributes) and observer:
216
+ Now, suppose you have defined the following model (with name and villain attributes) and observer:
205
217
 
206
218
  ```ruby
207
219
  class Wizard < ActiveRecord::Base
@@ -214,7 +226,7 @@ class WizardObserver < PowerTypes::Observer
214
226
  after_create :kill_villain
215
227
 
216
228
  def kill_villain
217
- p "#{object.name} have killed #{object.villian}"
229
+ p "#{object.name} has killed #{object.villain}"
218
230
  end
219
231
  end
220
232
  ```
@@ -222,43 +234,202 @@ end
222
234
  Then, you can use it like this:
223
235
 
224
236
  ```ruby
225
- Wizard.create!(name: "Gandalf", villian: "Sauron") #=> This action will trigger the method kill_villian defined in the WizardObserver's after_create callback.
237
+ Wizard.create!(name: "Gandalf", villain: "Sauron") #=> This action will trigger the method kill_villain defined in the WizardObserver's after_create callback.
226
238
  ```
227
239
 
228
240
  > As you can guess, `object` holds the Wizard instance.
229
241
 
230
- ### Values and Utils
242
+ You can trigger multiple methods on the same callback. For example:
231
243
 
232
- This two types do not have generators.
244
+ ```ruby
245
+ class WizardObserver < PowerTypes::Observer
246
+ after_create :kill_villain
247
+ after_create :bury_villains_corpse
248
+
249
+ def kill_villain
250
+ p "#{object.name} has killed #{object.villain}"
251
+ end
252
+
253
+ def bury_villains_corpse
254
+ p "#{object.name} has buried #{object.villain}'s corpse"
255
+ end
256
+ end
257
+ ```
258
+ Note: Triggering the event will preserve the order of the methods, so in the example `kill_villain` will be called before `bury_villains_corpse`.
259
+
260
+ Recently we added three new callbacks, `after_create_commit`, `after_update_commit` and `after_save_commit`. With these callbacks we want to reproduce the `after_commit` transactional callback from Active Record. For this implementation we use the gem [After Commit Everywhere](https://github.com/Envek/after_commit_everywhere) to be able to use the `after_commit` callbacks outside the Active Record models.
261
+
262
+ ### Values
263
+
264
+ This pattern doesn't have a generator.
233
265
 
234
266
  Values are just simple Ruby classes, but watch out to keep them in the Values directory!
235
267
 
236
- Utils should be defined as a module. There you define the independent but related functions. Use the extend self pattern to call them directly after the module name.
268
+ ### Presenters
269
+
270
+ For generating presenters we use:
271
+
272
+ ```
273
+ $ rails generate presenter users_show
274
+ ```
275
+
276
+ This will create the `UsersShowPresenter` class, inheriting from a base class:
277
+
278
+ ```ruby
279
+ class UsersShowPresenter < PowerTypes::PresenterBase
280
+ end
281
+ ```
282
+
283
+ And its corresponding rspec file:
284
+
285
+ ```ruby
286
+ require 'rails_helper'
287
+
288
+ describe UsersShowPresenter do
289
+ pending "add some examples to (or delete) #{__FILE__}"
290
+ end
291
+ ```
292
+
293
+ To initialize a presenter inside your controller action you should execute the `present_with` method with valid params:
294
+
295
+ ```ruby
296
+ class UsersController < InheritedResources::Base
297
+ def show
298
+ presenter_params = { param1: 1, param2: 2 }
299
+ @presenter = present_with(:users_show, presenter_params)
300
+ end
301
+ end
302
+ ```
303
+
304
+ You can access view helper methods through the `h` method:
305
+
306
+ ```ruby
307
+ class UsersShowPresenter < PowerTypes::PresenterBase
308
+ def platanus_link
309
+ h.link_to "Hi Platanus!", "https://platan.us"
310
+ end
311
+ end
312
+ ```
313
+
314
+ You can access `presenter_params` inside the presenter as an `attr_reader`
315
+
316
+ ```ruby
317
+ class UsersController < InheritedResources::Base
318
+ def show
319
+ presenter_params = { platanus_url: "https://platan.us" }
320
+ @presenter = present_with(:users_show, presenter_params)
321
+ end
322
+ end
323
+ ```
324
+
325
+ ```ruby
326
+ class UsersShowPresenter < PowerTypes::PresenterBase
327
+ def platanus_link
328
+ h.link_to "Hi Platanus!", platanus_url
329
+ end
330
+ end
331
+ ```
332
+
333
+ If the presenter param has a [decorator](https://github.com/drapergem/draper), the `attr_reader` will be decorated.
237
334
 
238
335
  ```ruby
239
- module MagicTricks
240
- extend self
336
+ class UsersController < InheritedResources::Base
337
+ def show
338
+ presenter_params = { user: user }
339
+ @presenter = present_with(:users_show, presenter_params)
340
+ end
241
341
 
242
- def dissappear(object)
243
- #blah blah
342
+ private
343
+
344
+ def user
345
+ @user ||= User.find!(params[:id])
244
346
  end
347
+ end
348
+ ```
245
349
 
246
- def shrink(children)
247
- #bleh bleeh
350
+ ```ruby
351
+ class UserDecorator < Draper::Decorator
352
+ delegate_all
353
+
354
+ def cool_view_name
355
+ "~º#{name}º~"
248
356
  end
357
+ end
358
+ ```
249
359
 
250
- def shuffle(cards)
251
- #blaah
360
+ ```ruby
361
+ class UsersShowPresenter < PowerTypes::PresenterBase
362
+ def platanus_link
363
+ h.link_to "Hi #{user.cool_view_name}!", platanus_url
252
364
  end
253
- end
365
+ end
366
+ ```
367
+
368
+ In the view, you can use it like this:
369
+
370
+ ```
371
+ <div><%= @presenter.platanus_link %></div>
372
+ ```
373
+
374
+ ### Utils
375
+
376
+ To generate a util we use:
377
+
378
+ ```
379
+ $ bundle exec rails g util Numbers clean double
254
380
  ```
255
381
 
256
- Example of calling a Util function:
382
+ This will generate the `NumbersUtil` class in the `app/utils` directory, as follows:
257
383
 
258
384
  ```ruby
259
- MagicTricks.dissapear(rabbit)
385
+ class NumbersUtil < PowerTypes::BaseUtil
386
+
387
+ def self.clean
388
+ # Method code goes here
389
+ end
390
+
391
+ def self.double
392
+ # Method code goes here
393
+ end
394
+
395
+ end
396
+ ```
397
+
398
+ And it will generate the spec file as well, in the `spec/utils` directory:
399
+
400
+ ```ruby
401
+ require 'rails_helper'
402
+
403
+ describe NumbersUtil do
404
+ describe '#clean' do
405
+ pending 'describe what the util method clean does here'
406
+ end
407
+
408
+ describe '#double' do
409
+ pending 'describe what the util method double does here'
410
+ end
411
+
412
+ end
413
+ ```
414
+
415
+ Every util will inherit from the class `PowerTypes::BaseUtil` which raises an error when the initialize method is called. The purpose of this is to ensure that all the utils methods work as class methods. Thus, there is no need to create an instance of the util to use its methods. For instance, we could use the `NumbersUtil` as follows:
416
+
417
+ ```ruby
418
+ NumbersUtil.clean('5.000') # -> 5000
419
+ NumbersUtil.double(100) # -> 200
260
420
  ```
261
421
 
422
+ ## Publishing
423
+
424
+ On master/main branch...
425
+
426
+ 1. Change `VERSION` in `lib/power-types/version.rb`.
427
+ 2. Change `Unreleased` title to current version in `CHANGELOG.md`.
428
+ 3. Run `bundle install`.
429
+ 4. Commit new release. For example: `Releasing v0.1.0`.
430
+ 5. Create tag. For example: `git tag v0.1.0`.
431
+ 6. Push tag. For example: `git push origin v0.1.0`.
432
+
262
433
  ## Contributing
263
434
 
264
435
  1. Fork it
@@ -1,12 +1,22 @@
1
1
  module PowerTypes
2
2
  class InitGenerator < Rails::Generators::Base
3
3
  desc "This generator creates the folder structure for the power-types gem"
4
+
4
5
  def create_folders
5
6
  empty_directory "app/commands/"
6
7
  empty_directory "app/services/"
7
8
  empty_directory "app/observers/"
9
+ empty_directory "app/presenters/"
8
10
  empty_directory "app/utils/"
9
11
  empty_directory "app/values/"
10
12
  end
13
+
14
+ def config_presenters
15
+ insert_into_file(
16
+ "app/controllers/application_controller.rb",
17
+ "\n include PowerTypes::Presentable",
18
+ after: "ActionController::Base"
19
+ )
20
+ end
11
21
  end
12
22
  end
@@ -0,0 +1,12 @@
1
+ module Rails
2
+ class PresenterGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path("../templates", __FILE__)
4
+
5
+ desc "This generator creates a new presenter at app/presenters"
6
+
7
+ def create_presenter
8
+ template('presenter.rb', "app/presenters/#{file_name.underscore}_presenter.rb")
9
+ template('presenter_spec.rb', "spec/presenters/#{file_name.underscore}_presenter_spec.rb")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ class <%= class_name %>Presenter < PowerTypes::BasePresenter
2
+ end
@@ -0,0 +1,5 @@
1
+ require 'rails_helper'
2
+
3
+ describe <%= class_name %>Presenter do
4
+ pending "add some examples to (or delete) #{__FILE__}"
5
+ end
@@ -0,0 +1,7 @@
1
+ class <%= class_name %>Util < PowerTypes::BaseUtil
2
+ <% attributes_names.each do |method_name| %>
3
+ def self.<%= method_name %>
4
+ # Method code goes here
5
+ end
6
+ <% end %>
7
+ end
@@ -0,0 +1,9 @@
1
+ require 'rails_helper'
2
+
3
+ describe <%= class_name %>Util do
4
+ <% attributes_names.each do |method_name| %>
5
+ describe '#<%= method_name %>' do
6
+ pending 'describe what the util method <%= method_name %> does here'
7
+ end
8
+ <% end %>
9
+ end
@@ -0,0 +1,14 @@
1
+ module Rails
2
+ class UtilGenerator < Rails::Generators::NamedBase
3
+ source_root File.expand_path('../templates', __FILE__)
4
+
5
+ argument :attributes, type: :array, default: [], banner: 'method method'
6
+
7
+ desc 'This generator creates a new util at app/utils'
8
+
9
+ def create_util
10
+ template('util.rb', "app/utils/#{file_name.underscore}_util.rb")
11
+ template('util_spec.rb', "spec/utils/#{file_name.underscore}_util_spec.rb")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ class PowerTypes::Error < RuntimeError; end
2
+ class PowerTypes::PresenterError < PowerTypes::Error; end
3
+ class PowerTypes::UtilError < PowerTypes::Error; end
@@ -0,0 +1,7 @@
1
+ module PowerTypes
2
+ class BaseUtil
3
+ def initialize
4
+ raise PowerTypes::UtilError.new('a util cannot be instantiated')
5
+ end
6
+ end
7
+ end
@@ -1,5 +1,7 @@
1
1
  module PowerTypes
2
2
  class Observer
3
+ include AfterCommitEverywhere
4
+
3
5
  attr_reader :object
4
6
 
5
7
  PowerTypes::Util::OBSERVABLE_EVENTS.each do |event|
@@ -34,5 +36,21 @@ module PowerTypes
34
36
  def initialize(_object)
35
37
  @object = _object
36
38
  end
39
+
40
+ PowerTypes::Util::OBSERVABLE_TRANSACTIONAL_EVENTS.each do |event|
41
+ PowerTypes::Util::OBSERVABLE_TYPES.each do |type|
42
+ next unless type == :after
43
+
44
+ method_name = "#{type}_#{event}"
45
+ callback = method_name.gsub('_commit', '')
46
+ define_singleton_method(method_name) do |method|
47
+ send(callback) { execute_method_after_commit(method) }
48
+ end
49
+ end
50
+ end
51
+
52
+ def execute_method_after_commit(method)
53
+ after_commit { send(method) }
54
+ end
37
55
  end
38
56
  end
@@ -0,0 +1,28 @@
1
+ module PowerTypes
2
+ class BasePresenter
3
+ def initialize(view, params = {})
4
+ @h = view
5
+
6
+ params.each_pair do |attribute, value|
7
+ if respond_to?(attribute, true)
8
+ raise PowerTypes::PresenterError.new(
9
+ "attribute #{attribute} already defined in presenter"
10
+ )
11
+ end
12
+
13
+ singleton_class.send(:attr_accessor, attribute)
14
+ instance_variable_set("@#{attribute}", decorated_value(value))
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :h
21
+
22
+ def decorated_value(value)
23
+ return value unless value.respond_to?(:decorate)
24
+
25
+ value.decorate
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ module PowerTypes
2
+ module Presentable
3
+ def present_with(presenter_name, data = {})
4
+ presenter_class_by_name(presenter_name).new(view_context, data)
5
+ end
6
+
7
+ def presenter_class_by_name(presenter_name)
8
+ class_name = presenter_name.to_s.classify
9
+ class_constant = class_name.safe_constantize
10
+
11
+ if class_constant.blank?
12
+ raise PowerTypes::PresenterError.new(
13
+ "missing #{class_name} presenter class"
14
+ )
15
+ end
16
+
17
+ class_constant
18
+ end
19
+ end
20
+ end
@@ -1,6 +1,7 @@
1
1
  module PowerTypes
2
2
  module Util
3
3
  OBSERVABLE_EVENTS = [:create, :update, :save, :destroy]
4
+ OBSERVABLE_TRANSACTIONAL_EVENTS = [:create_commit, :update_commit, :save_commit]
4
5
  OBSERVABLE_TYPES = [:before, :after]
5
6
  end
6
7
  end
@@ -1,3 +1,3 @@
1
1
  module PowerTypes
2
- VERSION = '0.3.1'
2
+ VERSION = '0.6.0'
3
3
  end
data/lib/power_types.rb CHANGED
@@ -1,10 +1,17 @@
1
- require "power_types/version"
2
- require "power_types/util"
3
- require "power_types/patterns/service"
4
- require "power_types/patterns/command"
5
- require "power_types/patterns/observer/observable"
6
- require "power_types/patterns/observer/observer"
7
- require "power_types/patterns/observer/trigger"
1
+ require 'active_support/all'
2
+ require 'after_commit_everywhere'
3
+
4
+ require 'power_types/version'
5
+ require 'power_types/util'
6
+ require 'power_types/errors'
7
+ require 'power_types/patterns/service'
8
+ require 'power_types/patterns/command'
9
+ require 'power_types/patterns/observer/observable'
10
+ require 'power_types/patterns/observer/observer'
11
+ require 'power_types/patterns/observer/trigger'
12
+ require 'power_types/patterns/presenter/base_presenter'
13
+ require 'power_types/patterns/presenter/presentable'
14
+ require 'power_types/patterns/base_util'
8
15
 
9
16
  module PowerTypes
10
17
  end
data/power-types.gemspec CHANGED
@@ -1,4 +1,3 @@
1
- # coding: utf-8
2
1
  lib = File.expand_path('../lib', __FILE__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'power_types/version'
@@ -19,16 +18,17 @@ Gem::Specification.new do |spec|
19
18
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
19
  spec.require_paths = ["lib"]
21
20
 
22
- spec.add_development_dependency "bundler", "~> 1.6"
23
- spec.add_development_dependency "rake", "~> 10.4"
24
- spec.add_development_dependency "rspec", "~> 3.1"
25
- spec.add_development_dependency "rspec-nc", "~> 0.2"
21
+ spec.add_dependency "activesupport"
22
+ spec.add_dependency "after_commit_everywhere", "~> 1.2", ">= 1.2.2"
23
+
24
+ spec.add_development_dependency "bundler", "~> 2.2.15"
25
+ spec.add_development_dependency "coveralls"
26
26
  spec.add_development_dependency "guard", "~> 2.11"
27
27
  spec.add_development_dependency "guard-rspec", "~> 4.5"
28
- spec.add_development_dependency "terminal-notifier-guard", "~> 1.6", ">= 1.6.1"
29
28
  spec.add_development_dependency "pry", "~> 0.10"
30
- spec.add_development_dependency "pry-remote", "~> 0.1"
31
- spec.add_development_dependency "pry-byebug", "~> 3.2"
32
- spec.add_development_dependency "pry-nav", "~> 0.2"
33
- spec.add_development_dependency "coveralls"
29
+ spec.add_development_dependency "rake", "~> 10.4"
30
+ spec.add_development_dependency "rspec", "~> 3.1"
31
+ spec.add_development_dependency "rspec_junit_formatter"
32
+ spec.add_development_dependency "rubocop", "0.66"
33
+ spec.add_development_dependency "rubocop-rspec"
34
34
  end