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

Developing and maintaining a platform with Rail...

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

Avatar for Simone Carletti

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