131
1/131

Ruby meetup-dry

Embed Size (px)

Citation preview

Page 1: Ruby meetup-dry

1/131

Page 2: Ruby meetup-dry

MeNikita Shilnikov

• github.com/flash-gordon

• Whatever developer

• dry-rb and rom-rb core team member

2/131

Page 3: Ruby meetup-dry

dry-rb

3/131

Page 4: Ruby meetup-dry

Thank you!

4/131

Page 5: Ruby meetup-dry

Web-application

5/131

Page 6: Ruby meetup-dry

request -> (application) -> response

6/131

Page 7: Ruby meetup-dry

response = application(request)

7/131

Page 8: Ruby meetup-dry

response = application.call(request)

8/131

Page 9: Ruby meetup-dry

9/131

Page 10: Ruby meetup-dry

class User < ApplicationRecord before_save :set_name

def set_name self.name = [first_name, last_name].join(' ') endend

10/131

Page 11: Ruby meetup-dry

class User < ApplicationRecord before_save :set_name, unless: -> u { u.source == 'github' }

def set_name self.name = [first_name, last_name].join(' ') endend

11/131

Page 12: Ruby meetup-dry

class CreateUserFromGithub # ...end

class CreateUser # ...end

12/131

Page 13: Ruby meetup-dry

class CreateUser def call(params) User.create(params) do |user| user.name = build_name(user) end end

def build_name(user) [user.first_name, user.last_name].join(' ') endend

13/131

Page 14: Ruby meetup-dry

Single responsibility principle

14/131

Page 15: Ruby meetup-dry

def create user = CreateUser.new.call(params)end

15/131

Page 16: Ruby meetup-dry

def create create_user = CreateUser.new user = create_user.call(params)end

16/131

Page 17: Ruby meetup-dry

object + SRP = function

17/131

Page 18: Ruby meetup-dry

def create create_user = proc do |params| name = params.values_at(:first_name, :last_name).join(' ') User.create(params.merge(name: name)) end user = create_user.call(params)end

18/131

Page 19: Ruby meetup-dry

def create user = create_user.call(params)end

def create_user delimiter = ' '

proc do |params| name = params.values_at(:first_name, :last_name).join(delimiter) User.create(params.merge(name: name)) endend

19/131

Page 20: Ruby meetup-dry

dry-rb

20/131

Page 21: Ruby meetup-dry

~20 gems

21/131

Page 22: Ruby meetup-dry

Each gem serves one purpose

22/131

Page 23: Ruby meetup-dry

Not a left-pad

23/131

Page 24: Ruby meetup-dry

Principles

• Functional objects

• Immutability

24/131

Page 25: Ruby meetup-dry

Functional objects

class CreateUser attr_reader :repo

def initialize(repo) @repo = repo end

def call(data) repo.create(data) endend

25/131

Page 26: Ruby meetup-dry

create_user = CreateUser.new(repo)

26/131

Page 27: Ruby meetup-dry

create_user.call(name: 'John')  

27/131

Page 28: Ruby meetup-dry

create_user.call(name: 'John')^^^^^^^^^^^ ^^^^^^^^^^^^^

28/131

Page 29: Ruby meetup-dry

create_user.call(name: 'John')^^^^^^^^^^^| |^^^^^^^^^^^^^   object | | data     

29/131

Page 30: Ruby meetup-dry

create_user.(name: 'John')^^^^^^^^^^^|^^^^^^^^^^^^^^  function | data      

30/131

Page 31: Ruby meetup-dry

create_user.(name: 'Wilhelm')

create_user.(name: 'Hendrik')

create_user.(name: 'Pieter')

create_user.(name: 'Pierre')

create_user.(name: 'Maria')

create_user.(name: 'Philipp')

create_user.(name: 'Albert')

create_user.(name: 'Karl')

31/131

Page 32: Ruby meetup-dry

app = Application.new

app.call(request_1)app.call(request_2)app.call(request_3)app.call(request_4)app.call(request_5)app.call(request_6)app.call(request_7)app.call(request_8)app.call(request_9)

32/131

Page 33: Ruby meetup-dry

application -> ... -> create_user -> ...

33/131

Page 34: Ruby meetup-dry

The big picture

app = Application.new

status, headers, body = app.call(request_1)

34/131

Page 35: Ruby meetup-dry

Stack

1. Route

2. Validate

3. Issue updates (if any)

4. Fetch data

5. Render

35/131

Page 36: Ruby meetup-dry

Route with dry-web-roda

36/131

Page 37: Ruby meetup-dry

dry-web-roda = dry-web + Roda

37/131

Page 38: Ruby meetup-dry

dry-web-roda

class Shop::Application < Dry::Web::Roda::Application

end

38/131

Page 39: Ruby meetup-dry

dry-web-roda

class Shop::Application < Dry::Web::Roda::Application route 'users' do |r| r.is do r.post do # create user end end endend

39/131

Page 40: Ruby meetup-dry

dry-web-roda

r.post do create_user = CreateUser.new create_user.call(r.params['user'])end

40/131

Page 41: Ruby meetup-dry

class CreateUser def call(input)

endend

41/131

Page 42: Ruby meetup-dry

class CreateUser def call(input) validate(input) endend

42/131

Page 43: Ruby meetup-dry

class CreateUser def call(input) something.call(input) endend

43/131

Page 44: Ruby meetup-dry

Something is a schema

44/131

Page 45: Ruby meetup-dry

Validate withdry-validation

45/131

Page 46: Ruby meetup-dry

dry-validationvalidate and coerce

46/131

Page 47: Ruby meetup-dry

Coercion

params[:user_id]

47/131

Page 48: Ruby meetup-dry

Coercion

params[:user_id].class # => ???

48/131

Page 49: Ruby meetup-dry

Coercion

params[:user_id].class

# => String | Fixnum | Hash | Array

49/131

Page 50: Ruby meetup-dry

Coercion

params[:user_id].to_i

50/131

Page 51: Ruby meetup-dry

Coercion

'abyr'.to_i # => 0

51/131

Page 52: Ruby meetup-dry

52/131

Page 53: Ruby meetup-dry

Coercion

Integer('40') # => 40

53/131

Page 54: Ruby meetup-dry

Coercion

Integer('abyr') # => ArgumentError

54/131

Page 55: Ruby meetup-dry

Coercion

Integer('050') # => 40

55/131

Page 56: Ruby meetup-dry

Coercion

Integer('050', 10) # => 50

56/131

Page 57: Ruby meetup-dry

Coercion

Integer(50, 10) # => ArgumentError

57/131

Page 58: Ruby meetup-dry

dry-validation

class CreateUser Schema = Dry::Validation.Form do

endend

58/131

Page 59: Ruby meetup-dry

dry-validation

class CreateUser Schema = Dry::Validation.Form do required(:name).filled(:str?)

endend

59/131

Page 60: Ruby meetup-dry

dry-validation

class CreateUser Schema = Dry::Validation.Form do required(:name).filled(:str?) required(:age).filled(:int?, gteq?: 18) endend

60/131

Page 61: Ruby meetup-dry

dry-validation

CreateUser::Schema.('name' => 'John', 'age' => '20').to_h

=> { name: "John", age: 20 }

61/131

Page 62: Ruby meetup-dry

dry-validation

CreateUser::Schema.('name' => 'John', 'age' => '20').to_h

=> { name: "John", age: 20 } ^^^^^^^

62/131

Page 63: Ruby meetup-dry

dry-validation

CreateUser::Schema.('name' => 'John', 'age' => '16').errors

=> { age: ["must be greater than or equal to 18"] }

63/131

Page 64: Ruby meetup-dry

dry-validation

CreateUser::Schema.('name' => 'John', 'age' => 'abyr').errors

=> { age: ["must be an integer"] }

64/131

Page 65: Ruby meetup-dry

dry-validation

Replaces Strong Parameters and ActiveModel::Validations

65/131

Page 66: Ruby meetup-dry

class CreateUser def call(input) validation = Schema.(input)

if validation.success? create_from_data(validation.output) end endend

66/131

Page 67: Ruby meetup-dry

def create_from_data(data) user_repo = ??? user_repo.create(data)end

67/131

Page 68: Ruby meetup-dry

def create_from_data(data) user_repo = User user_repo.create(data)end

68/131

Page 69: Ruby meetup-dry

def create_from_data(data) user_repo = UserRepository.new(rom) user_repo.create(data)end

69/131

Page 70: Ruby meetup-dry

dry-container

70/131

Page 71: Ruby meetup-dry

dry-container

Shop::Container = Dry::Container.newShop::Container.register(:user_repo) do UserRepository.new(rom)end

71/131

Page 72: Ruby meetup-dry

dry-container

def create_from_data(data) user_repo = Shop::Container.resolve(:user_repo) user_repo.create(data)end

72/131

Page 73: Ruby meetup-dry

class CreateUser attr_reader :user_repo

def initialize @user_repo = Shop::Container.resolve(:user_repo) end

# ...

def create_from_data(data) user_repo.create(data) endend

73/131

Page 74: Ruby meetup-dry

dry-auto_injectDI tool for Ruby

74/131

Page 75: Ruby meetup-dry

dry-auto_inject

Shop::Import = Dry::AutoInject(Shop::Container)

75/131

Page 76: Ruby meetup-dry

dry-auto_inject

class CreateUser include Shop::Import[repo: 'repositories.user']

def call(input) validation = Schema.(input)

if validation.success? repo.create(validation.output) end endend

76/131

Page 77: Ruby meetup-dry

dry-auto_inject

create_user = CreateUser.new

77/131

Page 78: Ruby meetup-dry

dry-auto_inject

create_user = CreateUser.new(repo: injection)

78/131

Page 79: Ruby meetup-dry

Shop::Container.namespace(:operations) do

register(:create_user) do

Operations::CreateUser.new

end

end

79/131

Page 80: Ruby meetup-dry

Shop::Container.namespace(:operations) do

register(:create_user) do

Shop::Operations::CreateUser.new

end

register(:create_post) do

Shop::Operations::CreatePost.new

end

register(:create_tag) do

Shop::Operations::CreateTag.new

end

register(:create_account) do

Shop::Operations::CreateAccount.new

end

register(:update_user) do

Shop::Operations::UpdateUser.new

end

register(:update_post) do

Shop::Operations::UpdatePost.new

end

register(:update_tag) do

Shop::Operations::UpdateTag.new

end

register(:update_account) do

Shop::Operations::UpdateAccount.new

end

end

80/131

Page 81: Ruby meetup-dry

dry-system

81/131

Page 82: Ruby meetup-dry

dry-system

module Shop class Container < Dry::Web::Container configure do config.auto_register = 'lib'.freeze config.system_dir = 'system'.freeze config.root = Pathname(__FILE__).dirname.join('../..') end

load_paths! 'lib' # Adds to $LOAD_PATH endend

82/131

Page 83: Ruby meetup-dry

dry-system

r.post do r.resolve 'operations.create_user' do |create| create.(r.params['user']) endend

83/131

Page 84: Ruby meetup-dry

dry-system

r.resolve

1. Uses require

2. Searches for operations/create_user.rb

3. Creates Operations::CreateUser

84/131

Page 85: Ruby meetup-dry

r.post do r.resolve 'operations.create_user' do |create| user = create.(r.params['user'])

r.redirect_to "/users/#{ user.id }" endend

85/131

Page 86: Ruby meetup-dry

class CreateUser def call(input) validation = Schema.(input)

if validation.success? repo.create(validation.output) else # ??? end endend

86/131

Page 87: Ruby meetup-dry

class CreateUser def call(input) validation = Schema.(input)

if validation.success? repo.create(validation.output) else validation end endend

87/131

Page 88: Ruby meetup-dry

dry-monads

88/131

Page 89: Ruby meetup-dry

dry-monads

89/131

Page 90: Ruby meetup-dry

dry-monads

Either = Left | Right

Right(user)Left(error)

90/131

Page 91: Ruby meetup-dry

dry-monads

def call(input) validation = Schema.(input)

if validation.success? user = repo.create(validation.output) Right(user) else Left(validation) endend

91/131

Page 92: Ruby meetup-dry

dry-matcher

92/131

Page 93: Ruby meetup-dry

dry-matcherclass CreateUser include Dry::Matcher.for(:call, with: Dry::Matcher::EitherMatcher)

def call(input) validation = Schema.(input)

if validation.success? user = repo.create(validation.output) Right(user) else Left(validation) end endend

93/131

Page 94: Ruby meetup-dry

dry-matcherr.post do r.resolve 'operations.create_user' do |create| create.(r.params['user']) do |m| m.success do |user| r.redirect_to "/users/#{ user.id }" end

m.failure do |validation| # render errors end end endend

94/131

Page 95: Ruby meetup-dry

Render with dry-view

95/131

Page 96: Ruby meetup-dry

dry-viewr.post do r.resolve 'operations.create_user' do |create| create.(r.params['user']) do |m| m.success do |user| r.redirect_to "/users/#{ user.id }" end

m.failure do |validation| r.view 'users.new', validation: validation end end endend

96/131

Page 97: Ruby meetup-dry

dry-view

module Views module Users class New < Shop::ViewController configure do |config| config.template = "users/new" end

expose :validation end endend

97/131

Page 98: Ruby meetup-dry

Business logic withdry-transaction

98/131

Page 99: Ruby meetup-dry

CreateUser = ValidateUser + PersistUser

99/131

Page 100: Ruby meetup-dry

dry-transaction

class CreateUser Transaction = Dry::Transaction(container: Shop::Container) do

end

end

100/131

Page 101: Ruby meetup-dry

dry-transaction

class CreateUser Transaction = Dry::Transaction(container: Shop::Container) do step :validate, with: 'operations.validate' step :persist, with: 'persistance.commands.create_user' end

end

101/131

Page 102: Ruby meetup-dry

dry-transaction

class CreateUser Transaction = Dry::Transaction(container: Shop::Container) do step :validate, with: 'operations.validate' step :persist, with: 'persistance.commands.create_user' end

def call(input, &block) Transaction.(input, validate: [Schema], &block) endend

102/131

Page 103: Ruby meetup-dry

dry-transaction

class CreateUser Transaction = Dry::Transaction(container: Shop::Container) do step :validate, with: 'operations.validate' step :persist, with: 'persistance.commands.create_user' end

def call(input, &block) Transaction.(input, validate: [Schema], &block) end ^^^^^^^^^^^^^^^^^^end

103/131

Page 104: Ruby meetup-dry

dry-transaction

class Validate def call(input, schema) validation = schema.call(input)

if validation.success? Right(validation.output) else Left(validation) end endend

104/131

Page 105: Ruby meetup-dry

Data objects with dry-struct

105/131

Page 106: Ruby meetup-dry

dry-struct

class User < Dry::Struct attribute :name, Types::Strict::String attribute :email, Types::Strict::String attribute :age, Types::Strict::Intend

106/131

Page 107: Ruby meetup-dry

dry-struct

user = User.new(name: 'John', email: '[email protected]', age: 33)=> #<User name="John" email="[email protected]" age=33>

user.name # => "John"user.email # => "[email protected]"

107/131

Page 108: Ruby meetup-dry

dry-struct

User.new(name: nil, email: '[email protected]', age: 33)

# => Dry::Struct::Error: [User.new]# nil (NilClass) has invalid type for :name

108/131

Page 109: Ruby meetup-dry

dry-struct

class User < Dry::Struct Name = Types::Strict::String

attribute :name, Name attribute :email, Types::Strict::String attribute :age, Types::Strict::Intend

109/131

Page 110: Ruby meetup-dry

dry-types

Name = Types::Strict::String.constrained(min_size: 3)

Name['Li']

# => Dry::Types::ConstraintError:# "Li" violates constraints (min_size?(3, "Li") failed)

110/131

Page 111: Ruby meetup-dry

dry-types

Source = Types::Strict::String.enum('web', 'github')Source['web'] # => "web"

Source['foo']

# => Dry::Types::ConstraintError: "foo" violates constraints

111/131

Page 112: Ruby meetup-dry

Recap

response = application.call(request)

action = router.call(env)

data = Schema.call(params)

user = create_user.call(data)

html = template.call(user)

112/131

Page 113: Ruby meetup-dry

Recap

response = application.call(request) ^^^^^^^^^^^       action = router.call(env) ^^^^^^         data = Schema.call(params) ^^^^^^    user = create_user.call(data) ^^^^^^^^^^^       html = template.call(user) ^^^^^^^^

113/131

Page 114: Ruby meetup-dry

Recap• dry-web-roda

• dry-validation

• dry-container

• dry-auto_inject

• dry-system

• dry-monads

• dry-matcher

• dry-view

• dry-transaction

• dry-struct

• dry-types

114/131

Page 115: Ruby meetup-dry

A new wave

115/131

Page 116: Ruby meetup-dry

No mutable state

116/131

Page 117: Ruby meetup-dry

No monkey patching

117/131

Page 118: Ruby meetup-dry

Toolset

118/131

Page 119: Ruby meetup-dry

Stuff that works

119/131

Page 120: Ruby meetup-dry

120/131

Page 121: Ruby meetup-dry

Can I use it? (Yes!)

121/131

Page 122: Ruby meetup-dry

122/131

Page 123: Ruby meetup-dry

Easy start with dry-validation anddry-container

123/131

Page 124: Ruby meetup-dry

Choose what works for you

124/131

Page 125: Ruby meetup-dry

125/131

Page 126: Ruby meetup-dry

Get involved!

• dry-rb.org

• discuss.dry-rb.org

• gitter.im/dry-rb/chat

126/131

Page 127: Ruby meetup-dry

Ruby doesn't suck (anymore)

127/131

Page 128: Ruby meetup-dry

Ruby = Smalltalk + ...

128/131

Page 129: Ruby meetup-dry

Ruby = Smalltalk + Perl + ...

129/131

Page 130: Ruby meetup-dry

Ruby = Smalltalk + Perl + Lisp

130/131

Page 131: Ruby meetup-dry

Thank you

• github.com/flash-gordon

• @NikitaShilnikov

• github.com/dry-rb

• icelab/dry-web-skeleton

• icelab/berg

131/131