Upgrade to Pro — share decks privately, control downloads, hide ads and more …

[SouthEastRuby 2018] The Gem Check: writing better Ruby gems

[SouthEastRuby 2018] The Gem Check: writing better Ruby gems

52cc8a838bf44a589d2572833b2dd1b9?s=128

Vladimir Dementyev

August 02, 2018
Tweet

Transcript

  1. GEMCHECK Vladimir Dementyev Writing better Ruby gems

  2. palkan_tula palkan SouthEastRuby ‘18 2 @palkan @palkan_tula Vladimir Dementyev 723

    551 283 372
  3. palkan_tula palkan SouthEastRuby ‘18 ! Moscow ✈ # New York

    ✈ Nashville 3
  4. palkan_tula palkan SouthEastRuby ‘18 4

  5. palkan_tula palkan SouthEastRuby ‘18 evilmartians.com 5

  6. palkan_tula palkan SouthEastRuby ‘18 evilmartians.com 6

  7. palkan_tula palkan SouthEastRuby ‘18 evilmartians.com/blog 7

  8. palkan_tula palkan SouthEastRuby ‘18 Brooklyn, NY evilmartians.com 8

  9. THIS TALK

  10. palkan_tula palkan SouthEastRuby ‘18 –Matz “Ruby is designed to make

    programmers happy.” 10
  11. palkan_tula palkan SouthEastRuby ‘18 “The more expressive language the much

    easier is to write terrible code” –Noah Gibbs 11
  12. None
  13. palkan_tula palkan SouthEastRuby ‘18 13 gemcheck.evilmartians.io

  14. SIMPLICITY

  15. palkan_tula palkan SouthEastRuby ‘18 –Alan Kay “Simple things should be

    simple, complex things should be possible.” 15
  16. FROM COMPLEX TO SIMPLE Part 1

  17. palkan_tula palkan SouthEastRuby ‘18 SIMPLE IS… 17 Less code Less

    thinking when writing code Less thinking when reading code
  18. palkan_tula palkan SouthEastRuby ‘18 SIMPLE IS… 18 Less code Less

    thinking when writing code Less thinking when reading code
  19. WHY??? So much BOILERplate

  20. palkan_tula palkan SouthEastRuby ‘18 BOILERPLATE 20 uri = URI('http: //example.com/index.json')

    params = { limit: 10, page: 3 } uri.query = URI.encode_ www_form(params) req = Net ::HTTP ::Get.new uri req.basic_auth 'user', 'pass' res = Net ::HTTP.start(uri.host, uri.port) do |http| http.request(req) end if res.is_a?(Net ::HTTPSuccess) JSON.parse(res.body) end
  21. palkan_tula palkan SouthEastRuby ‘18 BOILERPLATE 21 HTTParty.get( "http: //example.com/index.json", {

    limit: 10, page: 3 }, basic_auth: { username: "user", password: "pass" } )
  22. palkan_tula palkan SouthEastRuby ‘18 COMPLEX POSSIBLE 22 HTTParty.get( 'http: //example.com/index.json',

    { limit: 10, page: 3 }, basic_auth: { ...}, headers: { ...}, open_timeout: 2, read_timeout: 3 )
  23. palkan_tula palkan SouthEastRuby ‘18 23 Simple and elegant API on

    top of the complex but powerful one
  24. SENSIBLE DEFAULTS

  25. palkan_tula palkan SouthEastRuby ‘18 SENSIBLE DEFAULTS 25 class Post <

    ActiveRecord ::Base # in the world with no defaults self.table_name = "posts" belongs_to :user, foreign_key: :user_id, class_name: "User", primary_key: :id end Sensible?
  26. palkan_tula palkan SouthEastRuby ‘18 SENSIBLE DEFAULTS 26 class Post <

    ActiveRecord ::Base belongs_to :user end CONVENTION OVER CONFIGURATION
  27. palkan_tula palkan SouthEastRuby ‘18 SENSIBLE DEFAULTS 27 Method defaults Configuration

    defaults
  28. palkan_tula palkan SouthEastRuby ‘18 SENSIBLE DEFAULTS 28 # With Pundit

    class ProductsController < ApplicationController def create authorize Product end end # With ActionPolicy class ProductsController < ApplicationController def create # target class is inferred from controller authorize! end end
  29. palkan_tula palkan SouthEastRuby ‘18 SENSIBLE CONFIG 29 Cover the most

    popular use cases with the default configuration
  30. palkan_tula palkan SouthEastRuby ‘18 SENSIBLE CONFIG 30 Cover the most

    popular use cases with the default configuration Support popular environment variables out-of- the-box
  31. palkan_tula palkan SouthEastRuby ‘18 REDIS_URL 31 def determine_redis_provider ENV[ENV['REDIS_PROVIDER'] ||

    'REDIS_URL'] end self.redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379" # => No env support( ✅ Sidekiq ❌ Resque
  32. palkan_tula palkan SouthEastRuby ‘18 DATABASE_URL 32 def establish_connection( spec =

    ENV["DATABASE_URL"]) … end url = ENV['DATABASE_URL'] config['url'] ||= url if url ✅ ActiveRecord ✅ SequelRails (but not Sequel ❌)
  33. palkan_tula palkan SouthEastRuby ‘18 RAILS_MAX_THREADS 33 https://github.com/mperham/sidekiq/issues/3090

  34. palkan_tula palkan SouthEastRuby ‘18 SIMPLE IS… 34 Less code Less

    thinking when writing code Less thinking when reading code
  35. LEAST SURPRISE

  36. palkan_tula palkan SouthEastRuby ‘18 LEAST SURPRISE 36 1.nonzero? => 1

    0.nonzero? => nil 0.zero? => true 1.zero? => false
  37. palkan_tula palkan SouthEastRuby ‘18 LEAST SURPRISE 37 https://2018.rubyparis.org

  38. palkan_tula palkan SouthEastRuby ‘18 LEAST SURPRISE 38

  39. palkan_tula palkan SouthEastRuby ‘18 NAMING SURPRISE 39

  40. palkan_tula palkan SouthEastRuby ‘18 NIL SURPRISE 40 # Always return

    false! Why? def exists?(url) res = nil begin res = HTTParty.head(url, timeout: 2) rescue HTTParty ::Error => e end !res.nil? && res.ok? end
  41. palkan_tula palkan SouthEastRuby ‘18 NIL SURPRISE 41 Meaning Patching #

    Always return false! Why? def exists?(url) res = nil begin res = HTTParty.head(url, timeout: 2) rescue HTTParty ::Error => e end !res.nil? && res.ok? end # From: /bundle/gems/httparty-0.15.5/lib/httparty/response.rb @ line 58: # Owner: HTTParty ::Response def nil? response.nil? || response.body.nil? || response.body.empty? end
  42. MONKEY PATCHING

  43. palkan_tula palkan SouthEastRuby ‘18 ACTIVESUPPORT 43 spec.add_runtime_dependency "activesupport" 78 %

    22 % Yes No Top-5000 gems
  44. palkan_tula palkan SouthEastRuby ‘18 stop_active_support_everywhere 44 Not every Ruby application

    is a Rails application Consider using Refinements instead
  45. None
  46. palkan_tula palkan SouthEastRuby ‘18 REFINEMENTS 46 module Anyway ::HashExt refine

    Hash do def stringify_keys! keys.each do |key| val = delete(key) val.stringify_keys! if val.is_a?(Hash) self[key.to_s] = val end end end end https://github.com/palkan/anyway_config
  47. palkan_tula palkan SouthEastRuby ‘18 REFINEMENTS 47 class Anyway ::Config using

    Anyway ::Ext ::DeepDup using Anyway ::Ext ::Hash def self.attr_config(*args, **hargs) @defaults = hargs.deep_dup defaults.stringify_keys! @config_attributes = args + defaults.keys attr_accessor(*@config_attributes) end end https://github.com/palkan/anyway_config
  48. palkan_tula palkan SouthEastRuby ‘18 REFINEMENTS 48 unless "".respond_to?(:safe_constantize) require "action_policy/ext/string_constantize"

    using ActionPolicy ::Ext ::StringConstantize end https://github.com/palkan/action_policy
  49. palkan_tula palkan SouthEastRuby ‘18 REFINEMENTS 49 https://github.com/mperham/sidekiq/pull/3474

  50. palkan_tula palkan SouthEastRuby ‘18 SIMPLE IS… 50 Less code Less

    thinking when writing code Less thinking when reading code Less thinking when resolving issues
  51. palkan_tula palkan SouthEastRuby ‘18 DEBUG FLOW 51 https://twitter.com/binaryberry/status/883275676467564545

  52. MEANINGFUL ERRORS

  53. palkan_tula palkan SouthEastRuby ‘18 “Error classes for machines, error messages

    for humans.” 53
  54. palkan_tula palkan SouthEastRuby ‘18 MEANINGFUL ERRORS 54 # BAD def

    update_as(type:, **params) raise ArgumentError, "Unknown type" unless TYPES.include?(type) ... end # GOOD def update_as(type:, **params) raise ArgumentError,"Unknown type: #{type}" unless TYPES.include?(type) ... end
  55. palkan_tula palkan SouthEastRuby ‘18 MEANINGFUL ERRORS 55

  56. palkan_tula palkan SouthEastRuby ‘18 ACTIONABLE ERRORS 56 https://github.com/jules2689/extended_bundler-errors

  57. palkan_tula palkan SouthEastRuby ‘18 ACTIONABLE ERRORS 57 module ActionPolicy class

    UnknownRule < Error # ... def suggest(policy, error) suggestion = ::DidYouMean ::SpellChecker.new( dictionary: policy.public_methods ).correct(error).first suggestion ? "\nDid you mean? #{suggestion}" : "" end end end authorize! Post, to: :creates? # => ActionPolicy ::UnknownRule: Couldn't find rule 'creates?' for PostPolicy # Did you mean? create? https://github.com/palkan/action_policy
  58. palkan_tula palkan SouthEastRuby ‘18 HUMAN ERRORS 58 https://2018.rubyparis.org

  59. Part 2 FROM SIMPLE TO COMPLEX

  60. palkan_tula palkan SouthEastRuby ‘18 “Write code for others, not for

    yourself.” 60
  61. FLEXIBILITY

  62. palkan_tula palkan SouthEastRuby ‘18 FLEXIBILITY 62 Adapters

  63. palkan_tula palkan SouthEastRuby ‘18 “The adapter pattern is classified as

    a structural pattern that allows a piece of code talk to another piece of code that it is not directly compatible with.” 63 https://dev.to/kylegalbraith/how-to-use-the-excellent-adapter-pattern-and-why-you-should-2c31
  64. palkan_tula palkan SouthEastRuby ‘18 ADAPTERIZATION 64 # config/application.rb module YourApp

    class Application < Rails ::Application config.active_job.queue_adapter = :sidekiq end end
  65. palkan_tula palkan SouthEastRuby ‘18 ADAPTERIZATION 65 Library Dep E.g. DB,

    cache, API, other lib
  66. palkan_tula palkan SouthEastRuby ‘18 ADAPTERIZATION 66 module BeforeAll module RSpec

    def before_all(&block) before(:all) do ActiveRecord ::Base.connection.begin_transaction(joinable: false) instance_eval(&block) end after(:all) { ActiveRecord ::Base.connection.rollback_transaction } end end end https://test-prof.evilmartians.io/#/before_all
  67. palkan_tula palkan SouthEastRuby ‘18 ADAPTERIZATION 67 Library Dep E.g. DB,

    cache, API, other lib Adapter
  68. palkan_tula palkan SouthEastRuby ‘18 ADAPTERIZATION 68 module BeforeAll class <<

    self attr_accessor :adapter def begin_transaction adapter.begin_transaction end end module RSpec def before_all(&block) before(:all) do BeforelAll.begin_transaction instance_eval(&block) end end end end https://github.com/palkan/test-prof/pull/81
  69. palkan_tula palkan SouthEastRuby ‘18 FLEXIBILITY 69 Adapter Middleware Plugin

  70. palkan_tula palkan SouthEastRuby ‘18 Library 70 Core API Middleware API

    Application Plugin B Plugin A MIDDLEWARE
  71. palkan_tula palkan SouthEastRuby ‘18 MIDDLEWARE 71 # config.ru require './my_app'

    use Rack ::Debug run MyApp.new Sidekiq.configure_server do |config| config.server_middleware do |chain| chain.add Sidekiq ::OinkMiddleware, logger: :new_relic end end
  72. palkan_tula palkan SouthEastRuby ‘18 PLUGIN 72 class RubocopMarkdown < Rubocop

    ::Plugin default_config File.join( __dir __, "default.yml") enable_if ->(filename) { markdown?(filename) } pre_process_source RubocopMarkdown ::Preprocessor end https://github.com/rubocop-hq/rubocop/issues/6012 WIP
  73. TESTABILITY 2 unit tests. 0 integration tests.

  74. palkan_tula palkan SouthEastRuby ‘18 CASE: ACTIONCABLE 74 No test adapter

    https://github.com/rails/rails/pull/23211 No unit-testing support https://github.com/rails/rails/pull/27191
  75. palkan_tula palkan SouthEastRuby ‘18 CASE: ACTIONCABLE 75 github.com/palkan/action-cable-testing

  76. palkan_tula palkan SouthEastRuby ‘18 TESTABILITY 76 Custom matchers / assertions

    # Clowne specify do is_expected.to clone_association( :profile, clone_with: ProfileCloner ) end
  77. palkan_tula palkan SouthEastRuby ‘18 TESTABILITY 77 Custom matchers / assertions

    # ActiveModelSerializers test "should render post serializer" do get :index assert_serializer "PostSerializer" end
  78. palkan_tula palkan SouthEastRuby ‘18 TESTABILITY 78 Test helpers / mocking

    # Devise test "should be success" do sign_in user get :index assert_equal 200, response.status end # Fog Fog.mock!
  79. palkan_tula palkan SouthEastRuby ‘18 TESTABILITY 79 Test adapters # ActiveJob

    config.active_job.queue_adapter = :test # ActionMailer config.action_mailer.delivery_method = :test
  80. palkan_tula palkan SouthEastRuby ‘18 TESTABILITY 80 Test mode / configuration

    CarrierWave.configure do |config| config.enable_processing = false end Devise.setup do |config| config.stretches = Rails.env.test? ? 1 : 11 end Sidekiq ::Testing.fake!
  81. palkan_tula palkan SouthEastRuby ‘18 CASE: WRAPPER 81 module Resolver class

    << self def resolve(host) return "1.2.3.4" if @test == true Resolv.getaddress(host) end def enable_test! @test = true end end end
  82. palkan_tula palkan SouthEastRuby ‘18 CASE: ACTION POLICY 82 class PostsController

    < ApplicationController def update @post = Post.find(params[:id]) authorize! @post # ... end end describe PostsController do subject { patch :update, id: post.id, params: params } it "is authorized" do expect { subject }.to be_authorized_to(:update?, post) .with(PostPolicy) end end https://actionpolicy.evilmartians.io/#/testing
  83. palkan_tula palkan SouthEastRuby ‘18 CASE: ACTION POLICY 83 https://actionpolicy.evilmartians.io/#/testing module

    PerThreadCache # ... # Turn off by default in test env self.enabled = !(ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test") end
  84. WHAT ELSE?

  85. SUPPORTING DOCUMENTS

  86. palkan_tula palkan SouthEastRuby ‘18 DOCUMENTS 86 Readme Documentation (rubydoc.info, readthedocs.io,

    whatever) Wiki Examples / Demo applications “How it works?” Changelog
  87. palkan_tula palkan SouthEastRuby ‘18 https://gemcheck.evilmartians.io github.com/palkan/gem-check 87

  88. THANK YOU! Vladimir Dementyev evilmartians.com/blog @palkan @palkan_tula @evilmartians