Slide 1

Slide 1 text

GEMCHECK Vladimir Dementyev Writing better Ruby gems

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

palkan_tula palkan SouthEastRuby ‘18 4

Slide 5

Slide 5 text

palkan_tula palkan SouthEastRuby ‘18 evilmartians.com 5

Slide 6

Slide 6 text

palkan_tula palkan SouthEastRuby ‘18 evilmartians.com 6

Slide 7

Slide 7 text

palkan_tula palkan SouthEastRuby ‘18 evilmartians.com/blog 7

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

THIS TALK

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

palkan_tula palkan SouthEastRuby ‘18 13 gemcheck.evilmartians.io

Slide 14

Slide 14 text

SIMPLICITY

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

FROM COMPLEX TO SIMPLE Part 1

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

WHY??? So much BOILERplate

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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 )

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

SENSIBLE DEFAULTS

Slide 25

Slide 25 text

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?

Slide 26

Slide 26 text

palkan_tula palkan SouthEastRuby ‘18 SENSIBLE DEFAULTS 26 class Post < ActiveRecord ::Base belongs_to :user end CONVENTION OVER CONFIGURATION

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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 ❌)

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

LEAST SURPRISE

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

palkan_tula palkan SouthEastRuby ‘18 LEAST SURPRISE 38

Slide 39

Slide 39 text

palkan_tula palkan SouthEastRuby ‘18 NAMING SURPRISE 39

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

MONKEY PATCHING

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

MEANINGFUL ERRORS

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

palkan_tula palkan SouthEastRuby ‘18 MEANINGFUL ERRORS 55

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Part 2 FROM SIMPLE TO COMPLEX

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

FLEXIBILITY

Slide 62

Slide 62 text

palkan_tula palkan SouthEastRuby ‘18 FLEXIBILITY 62 Adapters

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

palkan_tula palkan SouthEastRuby ‘18 FLEXIBILITY 69 Adapter Middleware Plugin

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

TESTABILITY 2 unit tests. 0 integration tests.

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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!

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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!

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

WHAT ELSE?

Slide 85

Slide 85 text

SUPPORTING DOCUMENTS

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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