Upload
nikita-shilnikov
View
58
Download
1
Embed Size (px)
Citation preview
1/131
MeNikita Shilnikov
• github.com/flash-gordon
• Whatever developer
• dry-rb and rom-rb core team member
2/131
dry-rb
3/131
Thank you!
4/131
Web-application
5/131
request -> (application) -> response
6/131
response = application(request)
7/131
response = application.call(request)
8/131
9/131
class User < ApplicationRecord before_save :set_name
def set_name self.name = [first_name, last_name].join(' ') endend
10/131
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
class CreateUserFromGithub # ...end
class CreateUser # ...end
12/131
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
Single responsibility principle
14/131
def create user = CreateUser.new.call(params)end
15/131
def create create_user = CreateUser.new user = create_user.call(params)end
16/131
object + SRP = function
17/131
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
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
dry-rb
20/131
~20 gems
21/131
Each gem serves one purpose
22/131
Not a left-pad
23/131
Principles
• Functional objects
• Immutability
24/131
Functional objects
class CreateUser attr_reader :repo
def initialize(repo) @repo = repo end
def call(data) repo.create(data) endend
25/131
create_user = CreateUser.new(repo)
26/131
create_user.call(name: 'John')
27/131
create_user.call(name: 'John')^^^^^^^^^^^ ^^^^^^^^^^^^^
28/131
create_user.call(name: 'John')^^^^^^^^^^^| |^^^^^^^^^^^^^ object | | data
29/131
create_user.(name: 'John')^^^^^^^^^^^|^^^^^^^^^^^^^^ function | data
30/131
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
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
application -> ... -> create_user -> ...
33/131
The big picture
app = Application.new
status, headers, body = app.call(request_1)
34/131
Stack
1. Route
2. Validate
3. Issue updates (if any)
4. Fetch data
5. Render
35/131
Route with dry-web-roda
36/131
dry-web-roda = dry-web + Roda
37/131
dry-web-roda
class Shop::Application < Dry::Web::Roda::Application
end
38/131
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
dry-web-roda
r.post do create_user = CreateUser.new create_user.call(r.params['user'])end
40/131
class CreateUser def call(input)
endend
41/131
class CreateUser def call(input) validate(input) endend
42/131
class CreateUser def call(input) something.call(input) endend
43/131
Something is a schema
44/131
Validate withdry-validation
45/131
dry-validationvalidate and coerce
46/131
Coercion
params[:user_id]
47/131
Coercion
params[:user_id].class # => ???
48/131
Coercion
params[:user_id].class
# => String | Fixnum | Hash | Array
49/131
Coercion
params[:user_id].to_i
50/131
Coercion
'abyr'.to_i # => 0
51/131
52/131
Coercion
Integer('40') # => 40
53/131
Coercion
Integer('abyr') # => ArgumentError
54/131
Coercion
Integer('050') # => 40
55/131
Coercion
Integer('050', 10) # => 50
56/131
Coercion
Integer(50, 10) # => ArgumentError
57/131
dry-validation
class CreateUser Schema = Dry::Validation.Form do
endend
58/131
dry-validation
class CreateUser Schema = Dry::Validation.Form do required(:name).filled(:str?)
endend
59/131
dry-validation
class CreateUser Schema = Dry::Validation.Form do required(:name).filled(:str?) required(:age).filled(:int?, gteq?: 18) endend
60/131
dry-validation
CreateUser::Schema.('name' => 'John', 'age' => '20').to_h
=> { name: "John", age: 20 }
61/131
dry-validation
CreateUser::Schema.('name' => 'John', 'age' => '20').to_h
=> { name: "John", age: 20 } ^^^^^^^
62/131
dry-validation
CreateUser::Schema.('name' => 'John', 'age' => '16').errors
=> { age: ["must be greater than or equal to 18"] }
63/131
dry-validation
CreateUser::Schema.('name' => 'John', 'age' => 'abyr').errors
=> { age: ["must be an integer"] }
64/131
dry-validation
Replaces Strong Parameters and ActiveModel::Validations
65/131
class CreateUser def call(input) validation = Schema.(input)
if validation.success? create_from_data(validation.output) end endend
66/131
def create_from_data(data) user_repo = ??? user_repo.create(data)end
67/131
def create_from_data(data) user_repo = User user_repo.create(data)end
68/131
def create_from_data(data) user_repo = UserRepository.new(rom) user_repo.create(data)end
69/131
dry-container
70/131
dry-container
Shop::Container = Dry::Container.newShop::Container.register(:user_repo) do UserRepository.new(rom)end
71/131
dry-container
def create_from_data(data) user_repo = Shop::Container.resolve(:user_repo) user_repo.create(data)end
72/131
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
dry-auto_injectDI tool for Ruby
74/131
dry-auto_inject
Shop::Import = Dry::AutoInject(Shop::Container)
75/131
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
dry-auto_inject
create_user = CreateUser.new
77/131
dry-auto_inject
create_user = CreateUser.new(repo: injection)
78/131
Shop::Container.namespace(:operations) do
register(:create_user) do
Operations::CreateUser.new
end
end
79/131
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
dry-system
81/131
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
dry-system
r.post do r.resolve 'operations.create_user' do |create| create.(r.params['user']) endend
83/131
dry-system
r.resolve
1. Uses require
2. Searches for operations/create_user.rb
3. Creates Operations::CreateUser
84/131
r.post do r.resolve 'operations.create_user' do |create| user = create.(r.params['user'])
r.redirect_to "/users/#{ user.id }" endend
85/131
class CreateUser def call(input) validation = Schema.(input)
if validation.success? repo.create(validation.output) else # ??? end endend
86/131
class CreateUser def call(input) validation = Schema.(input)
if validation.success? repo.create(validation.output) else validation end endend
87/131
dry-monads
88/131
dry-monads
89/131
dry-monads
Either = Left | Right
Right(user)Left(error)
90/131
dry-monads
def call(input) validation = Schema.(input)
if validation.success? user = repo.create(validation.output) Right(user) else Left(validation) endend
91/131
dry-matcher
92/131
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
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
Render with dry-view
95/131
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
dry-view
module Views module Users class New < Shop::ViewController configure do |config| config.template = "users/new" end
expose :validation end endend
97/131
Business logic withdry-transaction
98/131
CreateUser = ValidateUser + PersistUser
99/131
dry-transaction
class CreateUser Transaction = Dry::Transaction(container: Shop::Container) do
end
end
100/131
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
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
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
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
Data objects with dry-struct
105/131
dry-struct
class User < Dry::Struct attribute :name, Types::Strict::String attribute :email, Types::Strict::String attribute :age, Types::Strict::Intend
106/131
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
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
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
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
dry-types
Source = Types::Strict::String.enum('web', 'github')Source['web'] # => "web"
Source['foo']
# => Dry::Types::ConstraintError: "foo" violates constraints
111/131
Recap
response = application.call(request)
action = router.call(env)
data = Schema.call(params)
user = create_user.call(data)
html = template.call(user)
112/131
Recap
response = application.call(request) ^^^^^^^^^^^ action = router.call(env) ^^^^^^ data = Schema.call(params) ^^^^^^ user = create_user.call(data) ^^^^^^^^^^^ html = template.call(user) ^^^^^^^^
113/131
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
A new wave
115/131
No mutable state
116/131
No monkey patching
117/131
Toolset
118/131
Stuff that works
119/131
120/131
Can I use it? (Yes!)
121/131
122/131
Easy start with dry-validation anddry-container
123/131
Choose what works for you
124/131
125/131
Get involved!
• dry-rb.org
• discuss.dry-rb.org
• gitter.im/dry-rb/chat
126/131
Ruby doesn't suck (anymore)
127/131
Ruby = Smalltalk + ...
128/131
Ruby = Smalltalk + Perl + ...
129/131
Ruby = Smalltalk + Perl + Lisp
130/131
Thank you
• github.com/flash-gordon
• @NikitaShilnikov
• github.com/dry-rb
• icelab/dry-web-skeleton
• icelab/berg
131/131