The Gem Check: writing better Ruby gems

A presentation at SouthEastRuby in August 2018 in Nashville, TN, USA by Vladimir Dementyev

Slide 1

Slide 1

GEMCHECK Writing better Ruby gems Vladimir Dementyev

Slide 2

Slide 2

Vladimir Dementyev 723 @palkan 551 @palkan_tula 283 372 2 palkan palkan_tula SouthEastRuby ‘18

Slide 3

Slide 3

! Moscow ✈ # New York ✈ Nashville 3 palkan palkan_tula SouthEastRuby ‘18

Slide 4

Slide 4

4 palkan palkan_tula SouthEastRuby ‘18

Slide 5

Slide 5

evilmartians.com 5 palkan palkan_tula SouthEastRuby ‘18

Slide 6

Slide 6

evilmartians.com 6 palkan palkan_tula SouthEastRuby ‘18

Slide 7

Slide 7

evilmartians.com/blog 7 palkan palkan_tula SouthEastRuby ‘18

Slide 8

Slide 8

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

Slide 9

Slide 9

THIS TALK

Slide 10

Slide 10

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

Slide 11

Slide 11

“The more expressive language the much easier is to write terrible code” –Noah Gibbs 11 palkan palkan_tula SouthEastRuby ‘18

Slide 12

Slide 12

Slide 13

Slide 13

gemcheck.evilmartians.io 13 palkan palkan_tula SouthEastRuby ‘18

Slide 14

Slide 14

SIMPLICITY

Slide 15

Slide 15

“Simple things should be simple, complex things should be possible.” –Alan Kay 15 palkan palkan_tula SouthEastRuby ‘18

Slide 16

Slide 16

Part 1 FROM COMPLEX TO SIMPLE

Slide 17

Slide 17

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

Slide 18

Slide 18

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

Slide 19

Slide 19

WHY??? So much BOILERplate

Slide 20

Slide 20

BOILERPLATE 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 20 palkan palkan_tula SouthEastRuby ‘18

Slide 21

Slide 21

BOILERPLATE HTTParty.get( “http:!//example.com/index.json”, { limit: 10, page: 3 }, basic_auth: { username: “user”, password: “pass” } ) 21 palkan palkan_tula SouthEastRuby ‘18

Slide 22

Slide 22

COMPLEX POSSIBLE HTTParty.get( ‘http:!//example.com/index.json’, { limit: 10, page: 3 }, basic_auth: {!!…}, headers: {!!…}, open_timeout: 2, read_timeout: 3 ) 22 palkan palkan_tula SouthEastRuby ‘18

Slide 23

Slide 23

Simple and elegant API on top of the complex but powerful one 23 palkan palkan_tula SouthEastRuby ‘18

Slide 24

Slide 24

SENSIBLE DEFAULTS

Slide 25

Slide 25

SENSIBLE DEFAULTS 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? 🤔 25 palkan palkan_tula SouthEastRuby ‘18

Slide 26

Slide 26

SENSIBLE DEFAULTS class Post < ActiveRecord!::Base belongs_to :user end N O TI N E V N R CO E N V O O I T A R U G I F N O C 26 palkan palkan_tula SouthEastRuby ‘18

Slide 27

Slide 27

SENSIBLE DEFAULTS Method defaults Configuration defaults 27 palkan palkan_tula SouthEastRuby ‘18

Slide 28

Slide 28

SENSIBLE DEFAULTS # 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 28 palkan palkan_tula SouthEastRuby ‘18

Slide 29

Slide 29

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

Slide 30

Slide 30

SENSIBLE CONFIG Cover the most popular use cases with the default configuration Support popular environment variables out-ofthe-box 30 palkan palkan_tula SouthEastRuby ‘18

Slide 31

Slide 31

REDIS_URL ✅ Sidekiq def determine_redis_provider ENV[ENV[‘REDIS_PROVIDER’] !|| ‘REDIS_URL’] end ❌ Resque self.redis = Redis.respond_to?(:connect) ? Redis.connect : “localhost:6379” #!=> No env support( 31 palkan palkan_tula SouthEastRuby ‘18

Slide 32

Slide 32

DATABASE_URL ✅ ActiveRecord def establish_connection( spec = ENV[“DATABASE_URL”]) … end ✅ SequelRails (but not Sequel ❌) url = ENV[‘DATABASE_URL’] config[‘url’] !!||= url if url 32 palkan palkan_tula SouthEastRuby ‘18

Slide 33

Slide 33

RAILS_MAX_THREADS https://github.com/mperham/sidekiq/issues/3090 33 palkan palkan_tula SouthEastRuby ‘18

Slide 34

Slide 34

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

Slide 35

Slide 35

LEAST SURPRISE

Slide 36

Slide 36

LEAST SURPRISE 1.nonzero? !=> 1 0.nonzero? !=> nil 0.zero? !=> true 1.zero? !=> false 36 palkan palkan_tula SouthEastRuby ‘18

Slide 37

Slide 37

LEAST SURPRISE https://2018.rubyparis.org 37 palkan palkan_tula SouthEastRuby ‘18

Slide 38

Slide 38

LEAST SURPRISE 38 palkan palkan_tula SouthEastRuby ‘18

Slide 39

Slide 39

NAMING SURPRISE 39 palkan palkan_tula SouthEastRuby ‘18

Slide 40

Slide 40

NIL SURPRISE # 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 40 palkan palkan_tula SouthEastRuby ‘18

Slide 41

Slide 41

NIL SURPRISE # Always return false! Why? def exists?(url) res = nil begin res = HTTParty.head(url, timeout: 2) rescue HTTParty!::Error !=> e end a e M g n ni c t Pa g n hi !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 41 palkan palkan_tula SouthEastRuby ‘18

Slide 42

Slide 42

MONKEY PATCHING

Slide 43

Slide 43

ACTIVESUPPORT spec.add_runtime_dependency “activesupport” Yes No 22 % 78 % Top-5000 gems 43 palkan palkan_tula SouthEastRuby ‘18

Slide 44

Slide 44

stop_active_support_everywhere Not every Ruby application is a Rails application Consider using Refinements instead 44 palkan palkan_tula SouthEastRuby ‘18

Slide 45

Slide 45

Slide 46

Slide 46

REFINEMENTS 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 46 palkan palkan_tula SouthEastRuby ‘18

Slide 47

Slide 47

REFINEMENTS 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 47 palkan palkan_tula SouthEastRuby ‘18

Slide 48

Slide 48

REFINEMENTS unless “”.respond_to?(:safe_constantize) require “action_policy/ext/string_constantize” using ActionPolicy!::Ext!::StringConstantize end https://github.com/palkan/action_policy 48 palkan palkan_tula SouthEastRuby ‘18

Slide 49

Slide 49

REFINEMENTS https://github.com/mperham/sidekiq/pull/3474 49 palkan palkan_tula SouthEastRuby ‘18

Slide 50

Slide 50

SIMPLE IS… Less code Less thinking when writing code Less thinking when reading code Less thinking when resolving issues 50 palkan palkan_tula SouthEastRuby ‘18

Slide 51

Slide 51

DEBUG FLOW https://twitter.com/binaryberry/status/883275676467564545 51 palkan palkan_tula SouthEastRuby ‘18

Slide 52

Slide 52

MEANINGFUL ERRORS

Slide 53

Slide 53

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

Slide 54

Slide 54

MEANINGFUL ERRORS # 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 54 palkan palkan_tula SouthEastRuby ‘18

Slide 55

Slide 55

MEANINGFUL ERRORS 55 palkan palkan_tula SouthEastRuby ‘18

Slide 56

Slide 56

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

Slide 57

Slide 57

ACTIONABLE ERRORS 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 57 palkan palkan_tula SouthEastRuby ‘18

Slide 58

Slide 58

HUMAN ERRORS https://2018.rubyparis.org 58 palkan palkan_tula SouthEastRuby ‘18

Slide 59

Slide 59

Part 2 FROM SIMPLE TO COMPLEX

Slide 60

Slide 60

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

Slide 61

Slide 61

FLEXIBILITY

Slide 62

Slide 62

FLEXIBILITY Adapters 62 palkan palkan_tula SouthEastRuby ‘18

Slide 63

Slide 63

“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.” https://dev.to/kylegalbraith/how-to-use-the-excellent-adapter-pattern-and-why-you-should-2c31 63 palkan palkan_tula SouthEastRuby ‘18

Slide 64

Slide 64

ADAPTERIZATION # config/application.rb module YourApp class Application < Rails!::Application config.active_job.queue_adapter = :sidekiq end end 64 palkan palkan_tula SouthEastRuby ‘18

Slide 65

Slide 65

ADAPTERIZATION Library Dep E.g. DB, cache, API, other lib 65 palkan palkan_tula SouthEastRuby ‘18

Slide 66

Slide 66

ADAPTERIZATION 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 66 palkan palkan_tula SouthEastRuby ‘18

Slide 67

Slide 67

ADAPTERIZATION Library Adapter Dep E.g. DB, cache, API, other lib 67 palkan palkan_tula SouthEastRuby ‘18

Slide 68

Slide 68

ADAPTERIZATION 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 68 palkan palkan_tula SouthEastRuby ‘18

Slide 69

Slide 69

FLEXIBILITY Adapter Middleware Plugin 69 palkan palkan_tula SouthEastRuby ‘18

Slide 70

Slide 70

MIDDLEWARE Application Plugin A Plugin B Library Middleware API Core API 70 palkan palkan_tula SouthEastRuby ‘18

Slide 71

Slide 71

MIDDLEWARE # 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 71 palkan palkan_tula SouthEastRuby ‘18

Slide 72

Slide 72

PLUGIN class RubocopMarkdown < Rubocop!::Plugin default_config File.join(!dir!, “default.yml”) enable_if !->(filename) { markdown?(filename) } pre_process_source RubocopMarkdown!::Preprocessor end P I W https://github.com/rubocop-hq/rubocop/issues/6012 72 palkan palkan_tula SouthEastRuby ‘18

Slide 73

Slide 73

TESTABILITY 2 unit tests. 0 integration tests.

Slide 74

Slide 74

CASE: ACTIONCABLE No test adapter https://github.com/rails/rails/pull/23211 No unit-testing support https://github.com/rails/rails/pull/27191 😞 74 palkan palkan_tula SouthEastRuby ‘18

Slide 75

Slide 75

CASE: ACTIONCABLE github.com/palkan/action-cable-testing 75 palkan palkan_tula SouthEastRuby ‘18

Slide 76

Slide 76

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

Slide 77

Slide 77

TESTABILITY Custom matchers / assertions # ActiveModelSerializers test “should render post serializer” do get :index assert_serializer “PostSerializer” end 77 palkan palkan_tula SouthEastRuby ‘18

Slide 78

Slide 78

TESTABILITY Test helpers / mocking # Devise test “should be success” do sign_in user get :index assert_equal 200, response.status end # Fog Fog.mock! 78 palkan palkan_tula SouthEastRuby ‘18

Slide 79

Slide 79

TESTABILITY Test adapters # ActiveJob config.active_job.queue_adapter = :test # ActionMailer config.action_mailer.delivery_method = :test 79 palkan palkan_tula SouthEastRuby ‘18

Slide 80

Slide 80

TESTABILITY 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! 80 palkan palkan_tula SouthEastRuby ‘18

Slide 81

Slide 81

CASE: WRAPPER 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 81 palkan palkan_tula SouthEastRuby ‘18

Slide 82

Slide 82

CASE: ACTION POLICY 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 82 palkan palkan_tula SouthEastRuby ‘18

Slide 83

Slide 83

CASE: ACTION POLICY module PerThreadCache # !!… # Turn off by default in test env self.enabled = !(ENV[“RAILS_ENV”] !== “test” !|| ENV[“RACK_ENV”] !== “test”) end https://actionpolicy.evilmartians.io/#/testing 83 palkan palkan_tula SouthEastRuby ‘18

Slide 84

Slide 84

WHAT ELSE?

Slide 85

Slide 85

SUPPORTING DOCUMENTS

Slide 86

Slide 86

DOCUMENTS Readme Documentation (rubydoc.info, readthedocs.io, whatever) Wiki Examples / Demo applications “How it works?” Changelog 86 palkan palkan_tula SouthEastRuby ‘18

Slide 87

Slide 87

https://gemcheck.evilmartians.io github.com/palkan/gem-check 87 palkan palkan_tula SouthEastRuby ‘18

Slide 88

Slide 88

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