顯示具有 Ruby 標籤的文章。 顯示所有文章
顯示具有 Ruby 標籤的文章。 顯示所有文章

2018年12月8日 星期六

rspec test protocal (interface) - part 2 - define our own DSL for rspec


接續上一篇 https://everyday1percent.blogspot.com/2018/05/rspec-test-protocal-interface.html


現在我們要做一個客製化的 rspec matcher,達到的效果如下:


```ruby
RSpec.describe FooService do

  describe "interfaces" do
     specify do
        expect(FooService).to has_implemented_interfaces :call, :call!
     end
  end

end
```

可以先參考 rspec 的 custom matcher 應該就可以做到了

https://relishapp.com/rspec/rspec-expectations/docs/custom-matchers/define-a-matcher-supporting-block-expectations

如果要做成 gem 的話可以參考 rspec-expectations 開放的 protocal

https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/matcher_protocol.rb

https://makandracards.com/makandra/662-write-custom-rspec-matchers

https://github.com/kucaahbe/rspec-html-matchers

2018年12月7日 星期五

Ruby instance_eval & block



```rb
class Test
  def initialize(&block)
    @name = "Hello World"

    instance_eval(&block) if block_given?
  end

  def name
    @name
  end
end
```

這樣是完全可以的,因為在 block 裡面的 `self` 變成了 `Test` instance

```rb
Test.new do
  puts name
end

# => Hello World
```

2018年7月22日 星期日

不要在 rake task 裡定義 method


因為 rake task 的 scope 是 global 的,如果在裡面定義 method 等於是為 Object class 定義 private method,污染全部的 code

可行的做法之一是把要跑的 task 寫成 service 執行

另外推薦的做法是使用 `Rake::DSL`

```ruby
# lib/tasks/bicycle.rake

class BicycleTasks
  include Rake::DSL

  def initialize
    namespace :bicycle do
      task :assemble do
        bicycle = Bicycle.new

        # Assemble the bicycle:
        attach_wheels(bicycle)
        attach_handlebars(bicycle)
        attach_brakes(bicycle)
      end
    end
  end

  private

  def attach_wheels(bicycle)
    # ...
  end

  def attach_handlebars(bicycle)
    # ...
  end

  def attach_brakes(bicycle)
    # ...
  end
end

# Instantiate the class to define the tasks:
BicycleTasks.new
```

https://supergood.software/dont-step-on-a-rake/

2018年6月10日 星期日

用 Ruby proc 做 success and error handler, like JS Promise


```rb

# A base class for all classes implement calls to API.
class ApiCall
  attr_reader :params

  def self.call(params)
    new(params).call
  end

  def initialize(params)
    @params = params
  end

  def call
    @res = execute
    self
  end

  def on_success
    yield @res if @res.success
    self
  end

  def on_error
    yield @res unless @res.success
    self
  end

  private

  def execute
    fail NotImplementedError
  end
end

StripeCall.(number: 'valid')
  .on_success { |response| puts response.body }
  .on_error { |response| puts response.body }
# => ok response

StripeCall.(number: 'invalid')
  .on_success { |response| puts response.body }
  .on_error { |response| puts response.body }
# => bad response
```


感覺有點借鑑 JS promise 的做法,傳 on_success 和 on_failure 的 proc 進到 method 當作 handler,之前有想過這種做法,剛好在 Ruby weekly 看到有人示範了,的確是減少了 if else 判斷,也省略了 response 這種 hidden context,不過最後為了寫的方便一點用了 method(:handle_success) 就沒有特別喜歡,但還是學習了這個用法,又是 meta programing 流派...


原文: https://railsguides.net/conditional-execution-with-dsl/


另外在 comment 裡看到一個 gem https://github.com/apneadiving/waterfall

也是挺有趣的,真的很有 functional programing 的味道

是不是大家開始慢慢被影響導致越來越多人用 Funtional Ruby 了呢~?最近也常常聽到和看到有人要用 Ruby 實現以及使用 elixir pipe operator XD

2018 Ruby Kaigi 網路上看到的懶人整理




  • Matz considering alias yied_self with then
  • Rib, an alternative for irb
  • Stripe team building Ruby type checker



https://blog.bigbinary.com/2018/05/31/rubykaigi-2018-day-one.html






http.rb replace net::http


作者認為 Net::Http 雖然是 standard lib 之一,但 API 設計很不好,底層實作也有很多覺得不滿的地方,包含 Net::Http 的 Error 封裝方式,讓人不知道怎麼 rescue

有機會可以試用看看

https://github.com/httprb/http

https://twin.github.io/httprb-is-great/


2018年5月30日 星期三

ruby proc 可以傳default 值since ruby 1.9

所以

scope :latest, ->(size=5) { recent.limit(size) }

這樣是可以的

好廢的一篇文

呵呵

2018年5月21日 星期一

一篇講 OO cohesion 和 coupling 簡單易懂的文章

一直想找個好方式跟同事解釋怎麼 decoupling 和 design better code,這篇文章涵蓋了最重要的兩個概念而且用很簡單易懂的 code 解釋了,很不錯。

cohesion: class 裡的 methods 是不是 share 一樣的 context?

coupling: dependency

http://www.rubyguides.com/2018/05/ruby-cohesion-and-coupling/

2018年5月19日 星期六

新的做 soft delete 的 gem - discard


https://github.com/jhawthorn/discard


沒有 default 加上 default scope 我覺得是很棒的決定,這個 gem 留給開發者更多的彈性實作 soft delete,我覺得在大多數的 case 下都是很夠用了,相較之下一樣是 soft delete 的 gem paranoia 就太多 magic 了



2018年5月18日 星期五

檢查 gem verison 的方法


在devise 裡看到的

```rb


  def self.rails51? # :nodoc:
    Rails.gem_version >= Gem::Version.new("5.1.x")
  end

  def self.activerecord51? # :nodoc:
    defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new("5.1.x")
  end

```


2018年5月9日 星期三

rspec test protocal (interface) - part 1 - define what's interface and find it


通常我們會希望繼承於 Base class 底下的 class 會有相同的 interface,for example, 每個 service 都要有一個 #call 的 instance method,應該要有個很簡單的方法來對這些 services 測試是否有 implement 這個 protocal,理想情況如下:


```rb
class BaseService
  def call
    raise "NotImplemented"
  end
end
class FooService < BaseService
  def call
    puts "called"
  end
end
class BarService < BaseService
end
module Callable
  def call
    puts "called from module"
  end
end
class IncludeService
  include Callable
end
class PrivateService
  private

  def call
    puts "private call"
  end
end


RSpec.describe FooService do

  describe "interfaces" do
     specify do
        expect(FooService).to has_interface :call
        expect(FooService).to has_implemented_interface :call
     end
  end

end
```

it_has_interface => 有 respond_to 這個 method 就行 (個人覺得沒什麼用...)
it_has_implemented_interface => 有自己 implement (排除母 class) 或是 include method 進來


it_has_implemented_interface 希望達到的效果是:

FooService 和 IncludeService 會 passed
BarService 和 PrivateService 會 failed


來看看要怎麼樣做 :D


已知使用 respond_to? 是不行的,因為母 class 定義了 call,所以

```ruby
2.4.4 :013 > f = FooService.new
 => #<FooService:0x00007fed06152098>
2.4.4 :014 > b = BarService.new
 => #<BarService:0x00007fed06148700>
2.4.4 :015 > f.respond_to?(:call)
 => true
2.4.4 :016 > b.respond_to?(:call)
 => true
```

用 method_defined? 當然也不行,因為都是繼承於 BaseService 所以都有 defined,但來試來試一下

```ruby
2.4.4 :017 > FooService.method_defined?(:call)
 => true
2.4.4 :018 > BarService.method_defined?(:call)
 => true
```

我們要找的是一個方法檢測子 class 是否有定義這個 method,不管是不是用 mixin 的方式加進去的,有幾個 methods 可以用

* public_methods
* instance_methods
* public_instance_methods

instance_methods default 還會包含 protected 的 methods,不如我們先來試試看 public_instance_methods

```ruby
2.4.4 :030 > FooService.public_instance_methods.include?(:call)
 => true
2.4.4 :031 > BarService.public_instance_methods.include?(:call)
 => true
2.4.4 :032 > IncludeService.public_instance_methods.include?(:call)
 => true
2.4.4 :033 > PrivateService.public_instance_methods.include?(:call)
 => false
```

看起來還是不行,讓我們換個方法,用 public_methods(false)


```ruby
2.4.4 :068 > FooService.new.public_methods(false).include?(:call)
 => true
2.4.4 :069 > BarService.new.public_methods(false).include?(:call)
 => false
2.4.4 :070 > IncludeService.new.public_methods(false).include?(:call)
 => false
2.4.4 :071 > PrivateService.new.public_methods(false).include?(:call)
 => false
```

結果 public_methods(false) 沒辦法讓 include 進來的 method 包含在裡面,為什麼呢?可以看一下 IncludeService 的 ancessor

```
2.4.4 :072 > IncludeService.ancestors
 => [IncludeService, Callable, Object, Kernel, BasicObject]
```

可以想像成 include 其實就是在 parent class 以及 child class 中間插隊,所以也是算在 ancestors 裡面。


所以看起來沒有任何 built-in 的 method 可以直接做到這個功能,那不如我們就自幹一個看看吧?XD

由於要 support module 裡的 methods 所以想到最原始的方法就是去找 ancestors 裡在 superclass 之下的每個 instance_methods, code example:

```ruby
class Utils
  class << self
    def get_public_instance_methods_without_parent_class(klass)
      countable_ancestors = klass.ancestors[0...klass.ancestors.index(klass.superclass)]
      countable_ancestors.flat_map do |ancestor_class|
        ancestor_class.public_instance_methods(false)
      end.uniq
    end
  end
end
```

這個版本目前只能對instance method 做檢測,不過對我們來說已經足夠了,以後再想辦法擴充,接下來就是來研究自定義 rspec DSL 的部分。


2018年4月26日 星期四

同時定義 class method 和 instance method 的潮潮寫法

You can delegate method to your `class` since your class is also a object in Ruby, fanny~


```ruby
class Foo
  def self.bar
    return "Hello"
  end
  delegate :bar, to: :class
end
```

```ruby
Foo.bar
# => "Hello"

Foo.new.bar
# => "Hello"
```


Awesome.

為子 class 寫 class method 在 class 裡調用


老生常談的小技巧了,剛好看到 kickstarer 的一段 code,貼過來回溫一下


```ruby
    # Define attributes to be serialize in the `data` column.
    # It generates setters and getters for those.
    #
    # Example:
    #
    # class MyEvent < Events::BaseEvent
    #   belongs_to :post
    #
    #   data_attributes :title, :description
    # end
    #
    # MyEvent.create!(post: post, title: "Hello", description: "", metadata: {})
    def self.data_attributes(*attrs)
      @data_attributes ||= []

      attrs.map(&:to_s).each do |attr|
        @data_attributes << attr unless @data_attributes.include?(attr)

        define_method attr do
          self.data ||= {}
          self.data[attr]
        end

        define_method "#{attr}=" do |arg|
          self.data ||= {}
          self.data[attr] = arg
        end
      end

      @data_attributes
    end
```

https://gist.github.com/pcreux/d094affd957a336af4f59b85f6ec0e6d


little funny learn from ruby private

I usually make my private methods like this:

```
class Foo
  private

  def private_method
  end
end
```

However, there is inline-style private method like this:

```
class Foo
  private def private_method
  end

  def puiblic_method_yo!
  end
end
```

Additionally, attr_reader is also available for inline-style private method:

```
class Foo
  private attr_reader :bar
end
```

So fancy, isn't it? lol

kickstarter 的 event sourcing approach

也解釋了一些 event sourcing 相關的東西,還不錯的深入淺出好文,而且還有 gif 圖加分XD

這張就很棒的解釋了他們所謂的 Aggregation (綠色, state), Reactor (黃色), Calculator (藍色箭頭), Event (藍色方塊)



https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224

2018年4月22日 星期日

Rspec aggregate_failures syntax


意思就是在 aggregate_failures block 底下任一個的 expectation 有 error 時會針對每個 expectation 顯示 error 的 message

example

```ruby
    it "returns false if any of balance, locked, saving is negative" do
      negative_balance_account = build(:account, balance: -1)
      negative_locked_account = build(:account, locked: -1)
      negative_saving_account = build(:account, saving: -1)

      aggregate_failures "testing all senarios" do
        expect(negative_balance_account.has_negative_asset?).to be true
        expect(negative_locked_account.has_negative_asset?).to be true
        expect(negative_saving_account.has_negative_asset?).to be true
      end
    end
```

假設這個 test failed 了,那 console 的訊息會是像這樣:

```
  1) Account#has_negative_asset? returns false if any of balance, locked, saving is negative
     Got 2 failures from failure aggregation block "testing all senarios".
     # ./spec/models/account_spec.rb:15:in `block (3 levels) in <top (required)>'

     1.1) Failure/Error: expect(negative_locked_account.has_negative_asset?).to be true

            expected true
                 got false
          # ./spec/models/account_spec.rb:17:in `block (4 levels) in <top (required)>'

     1.2) Failure/Error: expect(negative_saving_account.has_negative_asset?).to be true

            expected true
                 got false
          # ./spec/models/account_spec.rb:18:in `block (4 levels) in <top (required)>'
```

會顯示哪幾個 test failed 了

當有很多 test case 很相近其實可以寫在一個senario 裡時用 aggregate_failures 會讓 test 變得更簡潔,但要小心不要亂用,該分開的 test 還是用 it 分開比較好。


https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures

2018年4月3日 星期二

PaperTrail whodunnit find object (ex: find User object)


PaperTrail::Verison object 的 methods 可以去 PaperTrail::VersionConcern 裡面查

查了一下沒有可以找到 whodunnit object 的方法,只好自己加一個:

```rb
#initializers/paper_trail.rb
PaperTrail::Version.class_eval do
  def whodunnit_object
     User.find_by(id: whodunnit)
  end
end
```


http://blog.nrowegt.com/papertrail-polymorphic-whodunnit/

2018年2月24日 星期六

看到一個不錯的 mock example (RSpec)

之前一篇文章提到 RSpec 盡量不用 let, 那要用啥?可以用 method。

另外看到一個不錯的 mock example, 用 tap 來 return double 但是 yield Authentictor

```rb
def stub_authenticator(auth_hash)
  double('authenticator').tap do |authenticator|
    Authenticator.stub(:new).with(authenticator).and_return(authenticator)
  end
end

def stub_find_user(attributes = {})
  build_stubbed(:user, attributes).tap do |user|
    User.stub(:find).with(user.to_param).and_return(user)
  end
end
```

https://apidock.com/ruby/Object/tap

from https://thoughtbot.com/upcase/videos/rspec-best-practices

Mutation in Ruby, it tells why more and more people like pure functional (immutable function)

Some mind blown examples:


```rb
[1] pry(main)> list = Array.new(5, "foo")
# => ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> list[4].clear
# => ""
[3] pry(main)> list
# => ["", "", "", "", ""]
```

因為每個 default foo 都是同一個 object


以及

```rb
[1] pry(main)> master_list = Hash.new([])
# => {}
[2] pry(main)> master_list[:evens] << 2
# => [2]
[3] pry(main)> master_list[:odds] << 3
# => [2, 3]
[4] pry(main)> master_list
# => {}
[5] pry(main)> master_list[:evens] <<= 4
# => [2, 3, 4]
[6] pry(main)> master_list
# => {:evens=>[2, 3, 4]}
[7] pry(main)> master_list[:odds] <<= 5
# => [2, 3, 4, 5]
[8] pry(main)> master_list
# => {:evens=>[2, 3, 4, 5], :odds=>[2, 3, 4, 5]}
```

一樣的原理,每個 default array 都是同一個 object

一個避免這樣的做法是用 proc 確保每個 default 值是不同 object,例如:

```rb
Array.new(5) { "foo" }
Hash.new { [] }
```

ex:

```rb
[1] pry(main)> Array.new(5) { "foo" }
=> ["foo", "foo", "foo", "foo", "foo"]
[2] pry(main)> a = Array.new(5) { "foo" }
=> ["foo", "foo", "foo", "foo", "foo"]
[3] pry(main)> a[2].clear
=> ""
[4] pry(main)> a
=> ["foo", "foo", "", "foo", "foo"]
[5] pry(main)>
```


另外還有更有趣的「以你為 freeze 了是吧?Boom~!」

```rb
[1] pry(main)> CONSTANT = ["foo"]
# => ["foo"]
[2] pry(main)> CONSTANT << "bar"
# => ["foo", "bar"]
[3] pry(main)> CONSTANT.freeze
# => ["foo", "bar"]
[4] pry(main)> CONSTANT << "baz"
# RuntimeError: can't modify frozen Array
# from (pry):4:in `__pry__'
[5] pry(main)> CONSTANT[1] << "baz"
# => "barbaz"
[6] pry(main)> CONSTANT
# => ["foo", "barbaz"]
```

這是因為 freeze 是 freeze CONSTANT 這個 Array 的 object,沒有 freeze 裡面的 string,所以 string 還是 mutable

(Ice Nine gem for deep freezing)

為了拯救 developers 於水深火熱的加班 debug 地獄中,不需要 mutate 的時候就不要 mutate,寫每個 function 都想一下「這真的有必要 mutate 嗎?」

2018年1月30日 星期二

Ruby Array(params[:ids])


关于 Array(params[:ids] 这个用法,如果是 Array([1,2,3]) 会等同于 [1,2,3] 没变,但是 Array[nil] 会变成 [] 空数组,这可以让 .each 方法不会因为 nil.each 而爆错。如果不这样处理,在没有勾选任何 offer 就送出的情况,就会爆出 NoMethodError 错误。除非你额外检查 params[:id] 如果是 nil 就返回,但不如用 Array 来的精巧。

来自 ihower 大大在全栈营教材中的 Pro tips