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

Developing and maintaining a platform with Rails and Hanami (RailsConf 2016)

Developing and maintaining a platform with Rails and Hanami (RailsConf 2016)

99e0b39c091e10d9c7d4452a34ca52dc?s=128

Simone Carletti

May 06, 2016
Tweet

Transcript

  1. Developing and maintaining a pla/orm with Rails and Hanami Simone

    Carle, / / DNSimple
  2. Simone Carle8 @weppos

  3. None
  4. I want to tell you a story

  5. 2013 2016 2014

  6. 2013 2016 2014

  7. DNSimple approach to Rails Present Hanami Rails + Hanami

  8. DNSimple approach to Rails

  9. Be in control of your code Key maintenance principle

  10. Custom API Any external dependency we introduce, we wrap it

    behind a custom API.
  11. require 'phony'
 
 module Dnsimple
 
 # Phone is a

    wrapper for the current phone validation library.
 module Phone
 
 WHITELIST = {
 "+3912312345678" => "+39 12312345678"
 }
 
 # Makes a plausibility check on the number.
 def self.plausible?(number)
 whitelisted?(number) || 
 Phony.plausible?(number)
 end 
 
 def self.format(number)
 whitelisted?(number) || 
 Phony.formatted(Phony.normalize(number), parentheses: false)
 end
  12. • Side features or extensions can be introduced without an

    extensive change to the codebase • IncompaKbiliKes can be addressed in a single place • When and if needed, the external dependency can be replaced with liMle effort • TesKng doesn't require intensive stubbing
  13. module Dnsimple
 class AliasResolver
 
 class << self
 def adapter

    
 @adapter ||= NullAdapter.new 
 end 
 
 def resolve(name)
 adapter.resolve(name)
 end 
 
 def enable_test! 
 self.adapter = test_adapter.new 
 end 
 end 
 # NullAdapter is a special adapter
 # that discards every resolve request.
 class NullAdapter
 def resolve(name)
 Result.new(name, [], [])
 end 
 end 
 
 class TestAdapter
 def resolve(*)
 result = @stubs.shift
 result ? Result.new(*result) : Result.new 
 end 
 end 
 
 class GoAdapter
 BIN = File.join(Rails.root, "bin", "dsalias")
 
 def resolve(name)
 #
 end 
 end 

  14. Dnsimple::AliasResolver.enable_test!
 
 describe "a methods that interacts with the resolver"

    do 
 before do 
 AliasResolver.adapter.test :stub, "fully.qualified.host"
 end 
 
 it "does something" do 
 # ...
 end 
 end 

  15. AcDveRecord We have special convenKons and guidelines.

  16. • Methods defined in ActiveRecord::Base are not allowed outside the

    Models • Models MUST expose custom API to perform operaKons • Callbacks are allowed only for data integrity • Query methods in ActiveRecord::Base are not allowed outside the Models or Finders • Scopes can't be invoked directly outside Models or Finders and they exist only to support Finders
  17. require 'model_finder'
 
 class SuffixFinder
 include ModelFinder::Finder
 
 def self.find(identifier)


    find_by_tld(identifier)
 end 
 
 def self.find!(identifier)
 find(identifier) or not_found!(identifier)
 end 
 
 def self.find_by_tld(identifier)
 tld = identifier.to_s.downcase
 enabled.where(tld: tld).take
 end 
 
 def self.suffix_listing 
 scope.enabled
 end 
 def self.newgtld_suffixes 
 scope.new_tlds.enabled.order_alpha
 end 
 
 def self.gtld_suffixes 
 scope.global_tlds.enabled.order_alpha
 end 
 
 def self.cctld_suffixes 
 scope.cc_tlds.enabled.order_alpha
 end 
 
 def self.privacy_supported_tlds 
 scope.where(whois_privacy: true).pluck(:tld)
 end 
 
 private 
 
 def self.not_found!(identifier)
 raise(ActiveRecord::RecordNotFound, "TLD `#{identifier}` not found")
 end 
 
 end 

  18. These guidelines are the result of years of experiments, discussions

    and refactoring.
  19. MVC+

  20.  ȕ Ư Request Controllers Commands Services Models

  21.  ȕ Ư Request Controllers Commands Services Models

  22.  ȕ Ư Request Controllers Commands Services Models

  23. class DomainHostingController def new # ... end def create @result

    = DomainCreateCommand.execute(command_context, this_account, domain_params) @domain = @result.data if @result.successful? redirect_to domain_path(@domain) else format.html { render action: "new" } end end private def domain_params DomainParams.new(params) end end
  24.  ȕ Ư Request Controllers Commands Services Models

  25. class DomainCreateCommand include Command::Command include Dnsimple::AccountRules::CommandMethods def execute(account, domain_attributes) return

    if error_account_domainlimit(account, domain_attributes[:name]) domain_service = DomainService.new domain = domain_service.create(domain_attributes[:name], account, domain_attributes.to_h) if domain Activity.track_domain(context, domain, :create) Dnsimple::Notifier::Events::DomainCreate.new(context.actor, domain).deliver Dnsimple::Notifier::Events::ZoneCreate.new(context.actor, domain.zone).deliver else result.error = I18n.t("command.message_error_validation_failed") end rescue # something # set an appropriate error message ensure result.data = domain end end
  26.  ȕ Ư Request Controllers Commands Services Models

  27. class Dnsimple::Services::DomainService def initialize(secondary_dns_service = Dnsimple::Services::SecondaryDnsService.new) @secondary_dns_service = secondary_dns_service end

    def create(name, account, domain_attributes) ActiveRecord::Base.transaction do domain = Domain.new domain.create_domain(name, account, domain_attributes) zone_service = ZoneService.new zone_service.create(domain) end end def delete(domain) # ... end def lock(domain) # ... end end
  28. class Dnsimple::Services::ZoneService def create(domain) zone = Zone.new zone.create_zone(domain, domain.account) create_system_records(zone)

    refresh(zone) end def create_system_records(zone) # ... end def refresh(zone) # ... end end
  29.  ȕ Ư Request Controllers Commands Services Models

  30. class Domain # Creates a new domain. # # This

    method auto-assigns internal tokens used for hashing # and persists the object in the database. # # Returns true if the object is created, false otherwise. def create_domain(name, account, attributes = {}, options = {}) self.attributes = attributes self.name = self.class.nameize(name) self.account = account assign_token creating(options) do # ... end end # Changes the account ownership of this domain. # # Returns true if the object is updated, false otherwise. def move_to_account(account) update_attribute(:account_id, account.id) end end
  31. Why?  ȕ Ư Request Controllers Commands Services Models

  32. Why?  ȕ Ư Request Controllers Commands Services Models

  33. Why?  ȕ Ư Request Controllers Commands Services Models

  34. Why?  ȕ Ư Request Commands Services Models

  35. None
  36. hanam irb.org

  37. Components of Hanami ApplicaKons RouKng AcKons Views Models
 MigraKons Helpers

    Mailers Assets Command Line
  38. Key feature of Hanami Modular Architecture ComposiKon Test-friendly

  39. Why did adopted Hanami ?

  40. +

  41. Rails.application.routes.draw do scope as: 'api', constraints: Dnsimple::ApiRouteConstraint, format: false do

    # v2 mount Api::V2::App.new, at: '/v2' end # ... end Hanami::Router.define do get "/:account_id/contacts", to: "contacts#index", api: "listContacts" post "/:account_id/contacts", to: "contacts#create", api: "createContact" get "/:account_id/contacts/:contact_id", to: "contacts#show", api: "getContact" patch "/:account_id/contacts/:contact_id", to: "contacts#update", api: "updateContact" delete "/:account_id/contacts/:contact_id", to: "contacts#destroy", api: "deleteContact" get "/tlds", to: "tlds#index", api: "listTlds" get "/tlds/:tld_id", to: "tlds#show", api: "getTld" get "/whoami", to: "authentication_context#show", api: "whoami" post "/oauth/access_token", to: "oauths#access_token", api: "oauthToken" # ... end
  42. module Controllers::Domains
 class Create
 def call(params)
 @result = DomainCreateCommand.execute(
 command_context,

    
 this_account, 
 DomainParams.new(params))
 @domain = @result.data
 
 if @result.successful?
 render DomainSerializer.new(@domain), 201
 else 
 render ErrorSerializer.new(@result.error, @domain), end 
 end 
 end 
 end class DomainHostingController
 def create 
 @result = DomainCreateCommand.execute(
 command_context,
 this_account,
 domain_params)
 @domain = @result.data
 
 if @result.successful?
 redirect_to domain_path(@domain)
 else 
 format.html { render action: "new" }
 end 
 end 
 end
  43. module Api::V2
 module Controllers::RegistrarWhoisPrivacy
 
 class Enable
 include Hanami::Action
 before

    :require_subscription!, :require_good_standing! 
 
 def call(params)
 # code
 end 
 end 
 
 class Disable
 include Hanami::Action
 before :require_subscription! 
 
 def call(params)
 # code
 end 
 end 
 
 end 
 end
  44. module Api::V2
 Hanami::Controller.configure do 
 handle_exceptions Rails.env.production?
 
 prepare do

    
 include Rendering
 include Errors
 include NotFoundHandler
 include Authentication
 include Subscription
 include Throttling
 
 def validate_account! 
 (params[:account_id].present? && params[:account_id] != ACCOUNT_WILDCARD) or error(400, "Parameter `:account_id` is required")
 end 
 
 def command_context 
 actor = (authentication_context.user || authentication_context.account).to_actor
 Command::Context.new(actor: actor, authenticated: authentication_context)
 end 
 end 
 end 
 end
  45. Wrapping Up

  46. ! Experiment

  47. " CommuniDes Get influenced by other

  48. hMps:/ /dnsimple.com/slack

  49. # Programming Languages Get influenced by other

  50. Credits hMps:/ /www.flickr.com/photos/sakaida_design/17100388500/

  51. Thanks! DNSimple dnsimple.com @dnsimple Simone Carle8 simonecarle8.com @weppos