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)

Simone Carletti

May 06, 2016
Tweet

More Decks by Simone Carletti

Other Decks in Programming

Transcript

  1. 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
  2. • 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
  3. 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 

  4. 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 

  5. • 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
  6. 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 

  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. +

  13. 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
  14. 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
  15. 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
  16. 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