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

Rails Routing DSL

Rails Routing DSL

How exactly does the rails routing DSL parsed and executed? What are the downsides to the current implementation and how to refactor it into a much more modular codebase?

Ujjwal Thaakar

June 28, 2014
Tweet

More Decks by Ujjwal Thaakar

Other Decks in Programming

Transcript

  1. –Ryan Bigg “You can fit all of the people who

    understand ActionDispatch inside a taxi”
  2. A little background • All core components in Rails are

    a subclass of Railtie • Engines are subclasses of Railtie and responsible for routing and other request handling configuration • Rails apps are supercharged engines
  3. ActionDispatch • ActionDispatch is located within ActionPack • It is

    responsible for dispatching HTTP requests to appropriate controller actions • Responsible for parsing the routing map and generating url helpers
  4. Routes • All engines have a routing table accessible by

    the Rails.application.routes method • routes is an instance of ActionDispatch::RouteSet • routes.routes is an instance of Journey::Routes • routes.routes.routes is an array of Journey::Route
  5. Routing DSL • Probably everyone here has written a Rails

    app • You can’t write one without declaring at least one route • The DSL is flexible, intuitive, helpful and awesome
  6. Scope • All route declarations happen within an implicit or

    explicit scope • Scope is a set of properties that determine the default options of a route • Most essentially these are the path, controller and action properties
  7. Mapper • Call to draw instance execs the block on

    an instance of Mapper • Mapper holds global state e.g. current scope, declared concerns, nesting and the route set • Scope is a hash that is backed up before running new scopes and then restored
  8. def scope(*args) options = args.extract_options!.dup recover = {} options[:path] =

    args.flatten.join('/') if args.any? options[:constraints] ||= {} unless nested_scope? options[:shallow_path] ||= options[:path] if options.key?(:path) options[:shallow_prefix] ||= options[:as] if options.key?(:as) end if options[:constraints].is_a?(Hash) defaults = options[:constraints].select do |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) end (options[:defaults] ||= {}).reverse_merge!(defaults) else block, options[:constraints] = options[:constraints], {} end SCOPE_OPTIONS.each do |option| if option == :blocks value = block elsif option == :options value = options else value = options.delete(option) end if value recover[option] = @scope[option] @scope[option] = send("merge_#{option}_scope", @scope[option], value) end end yield self ensure @scope.merge!(recover) end
  9. Route generators • match is the basis of all route

    generators • get/post/put/patch/delete are wrappers around match • match normalizes everything and uses Mapping to create a normalized route and adds it to the RouteSet • RouteSet further normalizes the routes and adds them to it’s own set (Journey::Router)
  10. Problems • Implementation is both brittle and confusing - hard

    even for experienced developers to get there head around it • Aaron Patterson (author of Journey) calls Mapper the biggest pain point of ActionDispatch • Merging of scopes loses information that is useful when route is added to route set
  11. Problems continue • Because resources is just a macro you

    have no information on whether a route is singular or not • Normalization is redundant because routes are unaware of controllers and actions • Paths are parsed multiple times in each route set
  12. And they continue… • A route doesn’t know if it

    represents a resource. This has led to long standing problems • Problems like generating polymorphic urls with singular resources. See issue #1769 • Using hash to maintains scope makes the DSL hard to test and maintain
  13. Object Graph • Instead of a Hash - create an

    Object Graph • Treat the Mapper as just a root scope • Encapsulate common logic in AbstractScope
  14. The Refactor • Calls that add routes execute within a

    scope and have direct access to the scope properties within which they execute • AbstractScope has an ivar parent pointing to it’s parent scope or nil for root scopes
  15. And so will be resources now This allows us to

    remove a lot of normalization code and generate appropriate routes directly
  16. CollectionResourceScope • Declares routes for performing bulk operations on a

    resource • CRUD operations for multiple entities in one atomic call • Takes an ids parameter e.g. /posts/1,3,6..9,21…42
  17. How does it fit together? • Entire DSL is declared

    on the AbstractScope class instead of Mapper • Subclasses use the super implementation along with their own customizations • All properties on a scope are built by merging with from it's parent scope which does so by merging with it's own parent and up… • Global state like route set is accessed from parent unless specified during initialization