Code Monkey home page Code Monkey logo

helmsman's Introduction

Helmsman

It's time to take the helm and navigate your web server!

Dependencies?!

Lets get this out of the way.

Clojars Project

What is Helmsman?

Helmsman is a Ring compliant HTTP routing tool that leverages routes by keeping them data, using that data to manage routing, and providing that data in the request for whatever the developer decides to use it for. For example, you can mark routes with meta-data to tie them together to dynamically create things like breadcrumbs or navigation links. Helmsman's URI engine also provides functionality to calculate relative URIs, eliminating the need to rely on the hostname for linking.

Stanzas

Helmsman can define routes several different ways but, there are three different kinds of mechanisms that Helmsman uses to construct them. You have routes, contexts, and middlewares. All of these contructs have syntax which are dubbed Stanzas where are merely vectors.

Contexts

A context is a group that serve certain kinds of web requests of your choosing. These are not routes but, have the benefit being able to clump routes together for the purposes of common paths or for common middleware. Providing paths is not a requirement for contexts where it was while Helmsman was in alpha.

Where ```...`` represents additional, nested routes, middleware, or contexts, the following would be considered a context.

[...]
["user/about" ...]
[:context ...]
[:context "my-info" ..,]

Routes

A route is a HTTP endpoint but, has all of the same properties as a context which allows routes to be nested inside of each other based on common paths.

Routes always describe an HTTP method, but optionally a path.

[:get some-handler-fn]
[:get "profile" some-handler-fn]

Routes can also encompass several HTTP method at once by providing a set of HTTP methods. The :any keyword matches all HTTP methods.

[#{:get :post :put} some-handler-fn]
[:any some-handler-fn]

Routes are also contexts which can have routes under them.

[:get home-page-handler
 [:get "about-us" about-us-handler]
 [:get "contact-us" contact-us-handler]]

Middleware

Middleware are functions that sit between the the request coming in and the handler. These are functions that take in a handler plus any number of arguements which produce a function which take a single argument, a Ring request, and does whatever it wants with it. Helmsman puts all of this together into a prepared function call when a web request that matches a particular path comes in. Middleware Stanzas describe the function and the arguments following the handler function which gets provided when the routes are compiled. An example of this might look as follows:

[:get home-page-handler
 [[authorization-fn :require-login]
  ["my-info"
   [:get my-info-handler]
   [:post update-my-info]]
  [[require-roles #{:admin}]
   [:get "admin" admin-handler
   ...]]]]

Metadata

Just like regular Clojure metadata, this information is not part of the route but is there to describe the route. This information will be available when a request comes in and gets attached to the formed Helmsman route in the compiled routing set which is available to the request. There is a single middleware key that's special which is the :id key which is supposed to uniquely identify a route.

["my-routes"
 ^{:id :about-my-routes
   :nav-lists #{:my-navs}
 [:get "about" handler-fn]
 ^{:id :save-my-routes
   :nav-lists #{:my-navs}}
 [:post "save" save-handler-fn]]

In this case, two routes have unique IDs so requests can be referenced and used for linking using relative URIs as well as a :nav-lists identifier that could be used for dynamically building navigation lists by getting all routes based on the provided metadata. That's it. How this information is used is completely up to the developer implementing it.

Requests

Once a request has been matched up with a route, it gets dispathed with all appropriate middleware and eventually the handler if the middlewares allow. How middleware and handlers behave is exactly the same as any other ring-based web application but, provides some extra data pertaining to the available routes in the Ring request. Helmsman adds a map to the request with the key :helmsman. This map contains information pertaining to the route that matched to this request, the set of all the routes, as well as the broken-apart path that came in from the request.

The map currently contains the following keys:

  • :request-path is a vector that describes the URI of the request. So, a browser with a URI of admin/roles/1234/update would get turned into a vector that looks like ["admin" "roles" "1234" "update"] but, if there is a trailing slash, we capture it for the purposes of properly creating relative URIs: admin/roles/ => ["admin" "roles" ""] but, they're stripped out when matching routes.
  • :request-signature is a vector of characters that represents the first character of each URI segment or an identifier for a keyword in that position. This does not uniquely identify a route but offers a quick way of finding possible matches. This isn't currently used for anything.
  • :routing-set is a set of all of the compiled routes. This can be used for whatever purpose a developer sees fit but, is mainly used for navigation.
  • :current-route is the item from :routing-set that matches the current request.
  • :request-path-params is a map that matches any keyword URI segments in the currently matched route's path.

Navigation

One of the best parts about having route information handy at the time a request is made is that it enables the ability to use that route data to generate relative links for you based on the compiled routes.

So lets say you have some routes:

[^{:id :home}
 [:get home-page-handler]
 ["user"
  ^{:id :profile}
  [:get "profile" profile-handler]
  ^{:id :reset-password}
  [:get "reset-password" reset-password-handler]]]

So long as you have the request, you can use the following to create relative URI paths:

(require ['helmsman.navigation :refer ['get-relative-path 'assemble-relative-uri]])
(get-relative-path request :home)
(get-relative-path request :profile)
(get-relative-path request :reset-password)

Depending on :request-path, Helmsman automatically generates a relative URI to the route that has the provided id. There are other functions within helmsman.navigation and helmsman.request to get routes in different ways. helmsman.uri provides functionality for creating string URIs to be used. The helmsman.navigation/assemble-relative-uri function is used to contstruct a URL that may have keywords in it, which can be provided to the function as substitutions.

License

Copyright and credits

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

helmsman's People

Contributors

jrdoane avatar migeorge avatar moquist avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

jrdoane

helmsman's Issues

Simplify Helmsman Syntax

As it stands right now, you can define a route with any of Helmsman's supported HTTP methods like this: [:get "/" (constantly "foo")] however if you wanted to support both :get and :post, you need to write two different routes.

I suggest updating helmsman to support stanzas like this:
[#{:get :post} "/" (constantly "foo")]

Edit:
It appears to me that the syntax should be simplified in general, not just made to support multiple methods per controller and path/context.

Now:

[:get "foo" some-handler
  [:context "bar"
    [:get "" another-handler]
    [:post "" another-handler]]]

I might prefer something like this:

[:get "foo" some-handler
  [#{:get :post} "bar" another-handler]]

Or

[:get "foo" some-handler
  [:context "bar"
    [#{:get :post} another-handler]]]

Or

[:context "foo"
  [:get some-handler]
  [#{:get :post} "bar" another-handler]]

Or

["foo"
  [:get some-handler]
  [#{:get :post} "bar" another-handler]]

I think all of these are valid possibilities if Helmsman gets updated to support any of these.

Routing: Support using vars as well as fn references.

A recommendation was made on reddit that it would enhance readability for development if Helmsman definitions supported using vars as well as fn references.

For example, assuming we have some fn:

(defn some-fun [request]
  "Some web page")

We should be able to do either:

[:get "/some/path" some-fun]

or...

[:get "/some/path" #'some-fun]

To get the same result.

This may work already but it's not supported. We should confirm that this actually works before saying we support it.

helmsman.uri/path doesn't convert keyword uri segments to keywords.

Calling (helmsman.uri/path "/a/b/:c/:d") will result in ["a" "b" ":c" ":d"] when it should result in ["a" "b" :c :d].

Most things that use a uri-path will check to see if the string is a keyword, so no bugs are produced by this, but it would be nice to actually have the uri-path match each URI segment exactly.

Add a stack of meta data from each definition stanza below the current route.

As it stands right now, you only get the meta data for the route that you're routed to without knowing anything about meta data from parents routes. In the case of API calls and authentication, we may want something like authentication requires to cascade through nested routes.

I'm not sure how important this is right now, but it was something I would like to have had on the current project that I'm working on.

Keyworded URI segments aren't parsing

When using a route like [:get "/some/thing/:somevalue" a-fn-somewhere] the named param won't recognize that as part of the URI. It just gets unhappy and doesn't match and 404s.

Ditch compojure for route matching.

Update Helmsman to not need to use any fns, macros, or data from Compojure in addition to building a new route-matching scheme that leverages the uri-paths that Helmsman already has to generate.

This is to solve a couple problems:

  • To segregate route middleware and application middleware. (Middleware that runs before Helmsman routing that all routes will use or Middleware that runs after routing has been completed and not loading route-level middleware until the route is matched).
  • To not rely on Compojure which is not using the latest version of ring-core and to replace an archaic route composition scheme that may have performance implications down the road.
  • To simplify an already complex process of generating Compojure routes from our own Helmsman structure.
  • To leverage ring directly and to take full control of any and all requests before routing even occurs.

These reasons make it seem like a good idea to take more control of the web server and eliminating Compojure as a middleman.

Support vars for reload support

When I make a Helmsman route like so, the middleware doesn't get executed:

(defn a-md-fn
  [handler v]
  (fn [i]
    (assoc i :bar v))

[:context "admin"
  [#'a-md-fn :foo]
  [:get "home" (fn [req] {:status 200 :body (:bar req)})]]

If the middleware fn is actually a fn, it works. I suspect that Helmsman is stripping out the middleware itself since the first parameter isn't actually true when calling (fn?). This is not a problem with route handlers though.

It's probably important to support var substitution anywhere in a Helmsman stanza.

Minor typo in project.clj

-[compojure "1.1.6" :exclodes [ring/ring-core]]
+[compojure "1.1.6" :exclusions [ring/ring-core]]

helmsman.uri/common-path does not recognize keywords as being variable strings.

Each of the following statements should match on keywords versus strings, always.

user=> (helmsman.uri/common-path ["a" "b" "c" "d"] ["a" "b" :sdasd "f"])                                 
["a" "b"]   
user=> (helmsman.uri/common-path ["a" "b" :asdjklsa "d"] ["a" "b" "c" "f"])                              
["a" "b"]   
user=> (helmsman.uri/common-path ["a" "b" :keyword "d"] ["a" "b" :keyword "f"])                          
["a" "b" :keyword]
user=> (helmsman.uri/common-path ["a" "b" :keyword "d"] ["a" "b" :different-keyword "f"])                  
["a" "b"]

It appears that relative URIs and URI construction is unaffected by this bug, probably because of how helmsman.uri/path-divergence and helmsman.uri/path-convergance handles all of our relative URI needs, so we might be able to remove common-path all together as nothing seems to call it.

Ensure route lists are handled properly.

It appears that implicit same-level contexts fail to compile routes when using a list/vector inside of any given context. This is important because we may want to apply middleware with common paths but, not for everything with a common path.

Works:

[:context "admin"
  [:get "" some-handler]
  [:post "" another-handler]]

Doesn't work (but should):

[:context "admin"
 [[:get "" some-handler]
  [:post "" another-handler]]]

Workaround:

[:context "admin"
  [:context ""
    [:get "" some-handler]
    [:post "" another-handler]]]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.