A presentation at SouthEastRuby in in Nashville, TN, USA by Vladimir Dementyev
GEMCHECK Writing better Ruby gems Vladimir Dementyev
Vladimir Dementyev 723 @palkan 551 @palkan_tula 283 372 2 palkan palkan_tula SouthEastRuby ‘18
! Moscow ✈ # New York ✈ Nashville 3 palkan palkan_tula SouthEastRuby ‘18
4 palkan palkan_tula SouthEastRuby ‘18
evilmartians.com 5 palkan palkan_tula SouthEastRuby ‘18
evilmartians.com 6 palkan palkan_tula SouthEastRuby ‘18
evilmartians.com/blog 7 palkan palkan_tula SouthEastRuby ‘18
evilmartians.com Brooklyn, NY 8 palkan palkan_tula SouthEastRuby ‘18
THIS TALK
“Ruby is designed to make programmers happy.” –Matz 10 palkan palkan_tula SouthEastRuby ‘18
“The more expressive language the much easier is to write terrible code” –Noah Gibbs 11 palkan palkan_tula SouthEastRuby ‘18
gemcheck.evilmartians.io 13 palkan palkan_tula SouthEastRuby ‘18
SIMPLICITY
“Simple things should be simple, complex things should be possible.” –Alan Kay 15 palkan palkan_tula SouthEastRuby ‘18
Part 1 FROM COMPLEX TO SIMPLE
SIMPLE IS… Less code Less thinking when writing code Less thinking when reading code 17 palkan palkan_tula SouthEastRuby ‘18
SIMPLE IS… Less code Less thinking when writing code Less thinking when reading code 18 palkan palkan_tula SouthEastRuby ‘18
WHY??? So much BOILERplate
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
BOILERPLATE HTTParty.get( “http:!//example.com/index.json”, { limit: 10, page: 3 }, basic_auth: { username: “user”, password: “pass” } ) 21 palkan palkan_tula SouthEastRuby ‘18
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
Simple and elegant API on top of the complex but powerful one 23 palkan palkan_tula SouthEastRuby ‘18
SENSIBLE DEFAULTS
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
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
SENSIBLE DEFAULTS Method defaults Configuration defaults 27 palkan palkan_tula SouthEastRuby ‘18
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
SENSIBLE CONFIG Cover the most popular use cases with the default configuration 29 palkan palkan_tula SouthEastRuby ‘18
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
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
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
RAILS_MAX_THREADS https://github.com/mperham/sidekiq/issues/3090 33 palkan palkan_tula SouthEastRuby ‘18
SIMPLE IS… Less code Less thinking when writing code Less thinking when reading code 34 palkan palkan_tula SouthEastRuby ‘18
LEAST SURPRISE
LEAST SURPRISE 1.nonzero? !=> 1 0.nonzero? !=> nil 0.zero? !=> true 1.zero? !=> false 36 palkan palkan_tula SouthEastRuby ‘18
LEAST SURPRISE https://2018.rubyparis.org 37 palkan palkan_tula SouthEastRuby ‘18
LEAST SURPRISE 38 palkan palkan_tula SouthEastRuby ‘18
NAMING SURPRISE 39 palkan palkan_tula SouthEastRuby ‘18
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
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
MONKEY PATCHING
ACTIVESUPPORT spec.add_runtime_dependency “activesupport” Yes No 22 % 78 % Top-5000 gems 43 palkan palkan_tula SouthEastRuby ‘18
stop_active_support_everywhere Not every Ruby application is a Rails application Consider using Refinements instead 44 palkan palkan_tula SouthEastRuby ‘18
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
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
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
REFINEMENTS https://github.com/mperham/sidekiq/pull/3474 49 palkan palkan_tula SouthEastRuby ‘18
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
DEBUG FLOW https://twitter.com/binaryberry/status/883275676467564545 51 palkan palkan_tula SouthEastRuby ‘18
MEANINGFUL ERRORS
“Error classes for machines, error messages for humans.” 53 palkan palkan_tula SouthEastRuby ‘18
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
MEANINGFUL ERRORS 55 palkan palkan_tula SouthEastRuby ‘18
ACTIONABLE ERRORS https://github.com/jules2689/extended_bundler-errors 56 palkan palkan_tula SouthEastRuby ‘18
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
HUMAN ERRORS https://2018.rubyparis.org 58 palkan palkan_tula SouthEastRuby ‘18
Part 2 FROM SIMPLE TO COMPLEX
“Write code for others, not for yourself.” 60 palkan palkan_tula SouthEastRuby ‘18
FLEXIBILITY
FLEXIBILITY Adapters 62 palkan palkan_tula 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.” https://dev.to/kylegalbraith/how-to-use-the-excellent-adapter-pattern-and-why-you-should-2c31 63 palkan palkan_tula SouthEastRuby ‘18
ADAPTERIZATION # config/application.rb module YourApp class Application < Rails!::Application config.active_job.queue_adapter = :sidekiq end end 64 palkan palkan_tula SouthEastRuby ‘18
ADAPTERIZATION Library Dep E.g. DB, cache, API, other lib 65 palkan palkan_tula SouthEastRuby ‘18
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
ADAPTERIZATION Library Adapter Dep E.g. DB, cache, API, other lib 67 palkan palkan_tula SouthEastRuby ‘18
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
FLEXIBILITY Adapter Middleware Plugin 69 palkan palkan_tula SouthEastRuby ‘18
MIDDLEWARE Application Plugin A Plugin B Library Middleware API Core API 70 palkan palkan_tula SouthEastRuby ‘18
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
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
TESTABILITY 2 unit tests. 0 integration tests.
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
CASE: ACTIONCABLE github.com/palkan/action-cable-testing 75 palkan palkan_tula SouthEastRuby ‘18
TESTABILITY Custom matchers / assertions # Clowne specify do is_expected.to clone_association( :profile, clone_with: ProfileCloner ) end 76 palkan palkan_tula SouthEastRuby ‘18
TESTABILITY Custom matchers / assertions # ActiveModelSerializers test “should render post serializer” do get :index assert_serializer “PostSerializer” end 77 palkan palkan_tula SouthEastRuby ‘18
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
TESTABILITY Test adapters # ActiveJob config.active_job.queue_adapter = :test # ActionMailer config.action_mailer.delivery_method = :test 79 palkan palkan_tula SouthEastRuby ‘18
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
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
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
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
WHAT ELSE?
SUPPORTING DOCUMENTS
DOCUMENTS Readme Documentation (rubydoc.info, readthedocs.io, whatever) Wiki Examples / Demo applications “How it works?” Changelog 86 palkan palkan_tula SouthEastRuby ‘18
https://gemcheck.evilmartians.io github.com/palkan/gem-check 87 palkan palkan_tula SouthEastRuby ‘18
THANK YOU! Vladimir Dementyev evilmartians.com/blog @palkan @palkan_tula @evilmartians
Ruby gem is just a piece of code wrapped into a package, isn’t it? Yes. Technically.
On the other hand, we should think of gem as a code written for others to use. That’s why writing gems is more than just a mechanical process.
I’d like to share my checklist on how to write code for humans.
The following resources were mentioned during the presentation or are useful additional information.