Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core routing refactor #211

Merged
merged 160 commits into from Mar 29, 2017
Merged

Core routing refactor #211

merged 160 commits into from Mar 29, 2017

Conversation

bryanp
Copy link
Contributor

@bryanp bryanp commented Dec 5, 2016

It mostly works, but there are quite a few things to cleanup (see TODO comments). I summarized my thoughts in this discussion. Feel free to chime in!

  • feature tests
  • call handlers in current router first
    • and then only call parents, not routers outside of the current ancestry
  • request format matchers
  • new within method
  • configurable router priority
  • global handlers
  • inline api docs

Remaining Test Cases

  • that respond_to halts all on its own (int)
  • sending an argument of an unsupported type (int)
  • deleting cookie keys during request processing (int)
  • routing template definition (int)
  • know events defined on Pakyow::App (unit)
  • that Pakyow::App.reset calls super (unit)
  • Pakyow::App#initialize does the right things (unit)
  • config options / default values (unit)
  • Pakyow::App#call creates a new Pakyow::Controller (unit)
  • that requir_recursive is called when Pakyow::App loads (unit)
  • known events on Pakyow::Controller (unit)
  • Pakyow::Controller delegators, accessors (unit)
  • Pakyow::Controller.process does the right thing (unit)
  • Pakyow::Controller#initialize does the right thing (unit)
  • Pakyow::Router supported http methods, default extensions (unit)
  • Pakyow::Router delegators, accessors (unit)
  • Pakyow::Router initialization (unit)
  • router method_missing / respond_to_missing? (unit)

if @path.include?(":")
# replace named parameters with a named capture
reqex_path = @path.split("/").map { |segment|
if segment.include?(":")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be the same condition as on line 45 in populated_path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good catch! Actually, this might be prime for a refactor. We could add a method that yields each parameter segment and lets us provide a new value. I imagine it would be used here like this:

regex_path = replace_param_segments(@path) do |segment|
  '(?<' + segment[1..-1] + '>(\w|[-.~:@!$\'\(\)\*\+,;])*)'
end

And back in populated_path like this:

replace_param_segments(parameterized_path) do |segment|
  params[segment[1..-1].to_sym]
end

@jphager2
Copy link
Contributor

jphager2 commented Feb 18, 2017

bryanp and others added 24 commits March 4, 2017 07:31
It mostly works, but there are quite a few things to cleanup. Also needs
tests,  inline api documentation, and user-facing documentation.
Routes are no longer called in context of App. Instead, a new
Controller instance is created on every request. This commit also
introduces inline handlers and error mapping.
This commit changes the context in which routes are evaluated. Rather
than in a `Controller` instance, evaluation now occurs in a new
`Router` instance for each request.

These changes let us use routers much like normal classes. Functions
are now just methods that are called on a router instance. Modules can
be included into routers that define new methods, just like any PORO.
This will ultimately be replaced with pakyow-assets.
Controllers are now responsible for everything related to the
request/response lifecycle, including handing off from one router to
another and calling the matching route for a request. This makes router
more or less a container for holding routes and finding a match.

Routers and Routes now support generic matchers, rather than a path.
When passed a string path, a regex matcher will automatically be
created. Routers and Routes can also be defined with Regexp objects, or
any custom object that implements a `match` method.

Concerns of template expansion have been refactored out of Router
completely. No more heartburn over that \o/
@bryanp
Copy link
Contributor Author

bryanp commented Mar 27, 2017

🎉 Holy shit it's done. Leaving it open for review for a bit. Planning to merge Tuesday.

I'll likely squash before merging, because several (likely the majority) of the commits here add no value. If @jphager2 or anyone else has any hesitations with this decision, please speak up.

@bryanp bryanp changed the title [WIP] Routing refactor Refactor routing for 1.0 Mar 27, 2017
@bryanp bryanp changed the title Refactor routing for 1.0 1.0 core routing refactor Mar 27, 2017
@jphager2
Copy link
Contributor

@bryanp, thats okay by me. I will try to go through your recent commits today. Good work getting this done!

Copy link
Contributor

@jphager2 jphager2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bryanp, I think this is great work. Really appreciate the work on the docs as well. Good job getting this done!

#
after :configure do
if config.session.enabled
builder&.use config.session.object, config.session.options
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really okay to use &. here. Wouldn't it be a problem if builder isn't set?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah okay it's kind of a smell. Removed

def router
Router.instance
if config.protection.enabled
builder&.use(Rack::Protection, except: config.session.enabled ? [] : [:session_hijacking, :remote_token])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here too.

# Pakyow::App.router CustomMatcher.new do
# end
#
# Custom matchers can also make data available in +params+ by implementing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Do you already have a use case for this? I guess this can be used for constraints (e.g. that a path includes an accepted locale)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have come up with a couple contrived uses, but nothing that specifically led me to add this feature. Supporting custom matchers is really just a side-effect of our implementation.

# Pakyow::App.router CustomMatcher.new do
# end
#
# When defined on a Router, custom matchers should also implement +sub+,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be included in the example above?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good call. I'll add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this entire thing is incorrect. We don't currently support path modification from custom matchers. I decided that there wasn't a great reason to support this for now.

def initialize
@sets = {}
router = self
(class << Pakyow; self; end).send(:define_method, :Router) do |path, **hooks|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is equivalent to this (out of curriosity)?

Pakyow.singleton_class.class_eval do 
  define_method :Router do |path, **hooks|
    router.Router(path, **hooks)
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it and yes, it appears to be functionally equivalent. Will update

# end
# end
#
# @api public
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am just wondering if this really be public? Should we expect this to happen: router.method_missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I'll make it @api private.

return parent.find_router_by_name(names) if parent

first_name = names.shift
(routers || [self].concat(state.instances)).each do |router|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the world needs a blog post on stateful =).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay!

if route.is_a?(Proc)
context.instance_exec(&route)
else
context.__send__(route)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that sometimes send is used and here __send__. I prefer __send__, but maybe it would be best to only do it one way. Which is your preference?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__send__ is definitely my preference, given that I spent an hour or so figuring out why this wasn't working right. Let's standardize around __send__ in the framework.

end

def reprioritize!
@instances.sort! { |a, b|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Do you have a specific use case for this that you can share as an example? Will we need docs for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, router priority isn't defined! I'll add it.

The case that led to router priority is when a router defines a wildcard route. You usually want the wildcard to be the last route matched, because it's usually some sort of fallback. Using router priority, you can designate the router containing the wildcard as low priority.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. It's an interesting way to handle this problem. In Rails I think they add some methods for prepending/appending routes. Priorities is much better. Gives you more transparency and more control.

module WalkDir
DOT = ".".freeze

refine Dir.singleton_class do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curriosity, why not refine Dir instead of its singleton class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just did refine Dir it would add walk as an instance method. But we want it to be available on the class as Dir.walk.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. Thanks for the explanation. Still haven't used refinements too often. Quite useful though.

@bryanp bryanp merged commit 62d94df into environment Mar 29, 2017
@bryanp bryanp deleted the routing-refactor branch March 29, 2017 00:44
@bryanp bryanp changed the title 1.0 core routing refactor Core routing refactor Mar 29, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants