Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional Ruby, Rodakase, and another world of Ruby web applications

Tim Riley
December 12, 2015

Functional Ruby, Rodakase, and another world of Ruby web applications

A talk given at Rails Camp Canberra 2015.

Tim Riley

December 12, 2015
Tweet

More Decks by Tim Riley

Other Decks in Programming

Transcript

  1. ┌─────────┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─────────┐ │ Request │ │

    │ │ │ │ │ │ │ │Response │ │(params) │─>│*│─>│*│─>│*│─>│*│─>│ (HTML) │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────┘ └─┘ └─┘ └─┘ └─┘ └─────────┘
  2. rc_cities = ["Perth", "Sydney", "Canberra"].freeze rc_cities << "???" # =>

    RuntimeError: can't modify frozen Array require "ice_nine" IceNine.deep_freeze(rc_cities) rc_cities.last.upcase! # => RuntimeError: can't modify frozen String
  3. require "adamantium" class RailsCamp include Adamantium attr_reader :city def initialize(city)

    @city = city end end rc18 = RailsCamp.new("Canberra") rc18.city.upcase! # => RuntimeError: can't modify frozen String
  4. Functional objects State is for collaborators and config only. Everything

    else goes to #call. #call receives input and returns output.
  5. class SubscribeUser attr_reader :user def initialize(user) @user = user end

    def call SubscriptionRepository.save(user) end end
  6. class SubscribeUser attr_reader :user def initialize(user) # State (Ƙюя) @user

    = user end def call # Coupled to a concrete class (Ň•́є•̀Ň) SubscriptionRepository.new.save(user) end end
  7. class SubscribeUser attr_reader :subscription_repository def initialize(subscription_repository) # Collaborators as the

    only state (๑˃̵ᴗ˂̵)و @subscription_repository = subscription_repository end def call(user) # Input passed in subscription_repository.save(user) end end
  8. require "dry-container" class MyContainer extend Dry::Container::Mixin end MyContainer.register "subscription_repository" do

    require "subscription_repository" SubcriptionRepository.new end MyContainer.register "subscribe_user" do require "subscribe_user" SubscribeUser.new(MyContainer["subscription_repository"]) end MyContainer["subscribe_user"].call(user)
  9. Rodakase → Container as central organisational structure → Simple, side-effect-less,

    functional objects → Routing/HTTP decoupled from core logic → Request processing as series of function calls
  10. Auto-registration module Main class Container < Rodakase::Container setting :auto_register, %w(

    lib/main/operations lib/main/validation lib/main/views ) end # `Main::Views::Home` defined in lib/main/views/home.rb Main::Container["main.views.home"] # => #<Main::Views::Home>
  11. class MyApp < Roda route do |r| # /hello branch

    r.on "hello" do # Set variable for all routes in /hello branch @greeting = 'Hello' # GET /hello/world request r.get "world" do "#{@greeting} world!" end end end end
  12. Rodakase application module Main class Application < Rodakase::Application configure do

    |config| config.routes = "web/routes".freeze config.container = Container end use Rack::Session::Cookie, key: "my_app.session", secret: MyApp::Container.config.app.session_secret route do |r| r.multi_route end load_routes! end end
  13. routes/users.rb route "users" do |r| r.on "new" do r.is to:

    "main.views.users.new" end r.is to: "main.views.users.index" end
  14. Rodakase views require "main/import" require "main/view" # In module Main::Views::Users

    class Index < Main::View include Main::Import("main.persistence.repositories.users") configure do |config| config.template = "users/index" end def locals(options = {}) {users: users.index(page: options[:page], per_page: options[:per_page])} end end
  15. Templates / users/index.slim .users == users.table / users/index/_table.slim table -

    users.each do |user| == user.row / users/index/_row.slim tr td = user.name td = user.email
  16. Transactions route "users" do |r| r.post do r.resolve "main.transactions.create_user" do

    |create_user| create_user.(r[:user]) do |m| m.success do r.redirect "/users" end m.failure do |errors| r.resolve "main.views.users.new" do |view| view.(params: r[:user], errors: errors) end end end end end end
  17. Multi-step transaction using Call Sheet Main::Transactions.define do |t| t.define "main.transactions.create_user"

    do step :persist, with: "main.operations.create_user" tee :deliver_email, with: "main.operations.deliver_welcome_email" end end
  18. require "kleisli" class CreateUser include Main::Import("main.validation.user_form_schema", "persistence.create_user") def call(params =

    {}) validation = user_form_schema.(params) if validation.messages.any? Left(validation.messages) else result = create_user.(validation.params) Right(Entities::User.new(result)) end end end
  19. That Pythagoreans say the first thing that came into existence

    was the monad. The monad begat the dyad, which begat the numbers, which begat the point, begetting lines or finiteness, etc. !
  20. route "users" do |r| r.post do r.resolve "main.transactions.create_user" do |create_user|

    create_user.(r[:user]) do |m| m.success do r.redirect "/users" end m.failure do |errors| r.resolve "main.views.users.new" do |view| view.(params: r[:user], errors: errors) end end end end end end
  21. Relations module Persistence module Relations class Users < ROM::Relation[:sql] dataset

    :users register_as :users use :pagination per_page 40 def by_id(id) where(id: id) end end end end
  22. Repositories require "main/entities/user" module Main module Persistence module Repositories class

    Users < ROM::Repository relations :users def [](id) users.by_id(id).as(Entities::User).one end def by_email(email) users.where(email: email).as(Entities::User).first end def index(page: 1, per_page: 40) users.per_page(per_page).page(page).as(Entities::User) end end end end end
  23. Commands require "dry-data" module Persistence module Commands class CreateUser <

    ROM::Commands::Create[:sql] input Dry::Data["hash"].schema( name: "string", email: "string", encrypted_password: "string", ) relation :users register_as :create result :one end end end
  24. dry-validation require "dry-validation" require "dry/validation/schema/form" module Main module Validation class

    UserFormSchema < Dry::Validation::Schema::Form key(:name) { |name| name.filled? } key(:email) { |email| email.filled? } key(:age) { |age| age.int? & age.filled? & age.gt?(20) } key(:admin) { |admin| admin.bool? } confirmation(:password) end end end
  25. dry-data require "dry-data" require "dry-equalizer" module Main module Entities class

    User < Dry::Data::Struct include Dry::Equalizer.new(:id, :name, :email, :encrypted_password) attribute :id, "int" attribute :name, "string" attribute :email, "string" attribute :encrypted_password, "string" end end end
  26. % rake spec Finished in 0.74344 seconds (files took 1.1

    seconds to load) 11 examples, 0 failures 1.1 seconds
  27. So many wins → Testing → Console driving → Separation

    of HTTP handling → Standalone views → Design → Maintainability