Last active
August 29, 2015 14:17
-
-
Save blaix/10464939aa9712887a1b to your computer and use it in GitHub Desktop.
A ruby web-dev framework that favors lots of small objects and dependency injection over DSLs and magic methods.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "wren/application" | |
# The simplest Wren application has a route and a handler: | |
class HelloApp | |
extend Wren::Routing # provides get/put/post/etc methods | |
extend Wren::Application # provides handle and mount methods | |
# and a rack-compatible call method | |
handle get("/hello/:name"), with: -> (name:, request:, response:) { | |
response.text("Hello #{name}!") | |
} | |
end | |
# Wren apps are rack compatible. In config.ru: | |
run HelloApp.new | |
# A handler can be any object that responds to `call` and returns a rack | |
# compatible response (or an object to hand to a responder, but we'll get to | |
# that later). This makes it very easy to decouple your application logic from | |
# the application framework: | |
module Hello | |
class SayHello | |
def call(name:, request:, response:) | |
response.text("Hello #{name}!") | |
end | |
end | |
class Application | |
extend Wren::Routing | |
extend Wren::Application | |
handle get("/hello/:name"), with: SayHello.new | |
end | |
end | |
# It looks weird that a SayHello class is concerned with request/response. | |
# Let's refactor our app to use a responder, which is a special type of handler | |
# that has an additional call paramter to accept whatever object s returned by | |
# the handler in front of it. | |
module Hello | |
class SayHello | |
def call(name:, **) | |
"Hello #{name}!" | |
end | |
end | |
class RespondWithText | |
def call(text, response:, **) | |
response.text(text) | |
end | |
end | |
class Application | |
extend Wren::Routing | |
extend Wren::Application | |
handle get("/hello/:name"), with: SayHello.new, | |
then: RespondWithText.new | |
end | |
end | |
# Now we have small, highly-focused objects that can be wired together by the | |
# Application class. Your handlers can be unit tested in isolation, and your | |
# integration tests can hit the application object. Wren encourages this type | |
# of design and provides other features to support it. | |
# Let's go full-on MVC and add a model and a view: | |
require "wren/views/delegate" | |
module Hello | |
Person = Struct.new(:name) | |
class SayHello | |
def call(name:, response:, **) | |
person = Person.new(name) | |
# Wren's delegate view is a simple delegator with a render method that | |
# forwards template variables as messages to the passed-in object: | |
view = Wren::Views::Delegate.new(person) | |
# Views render templates with themselves as the context. | |
# So if the template looks like this: <h1>{{name}}</h1> | |
# then {{name}} will be replaced with `view.name` which in this case | |
# delegates to `person.name`. | |
# | |
# Any class can be a view if it includes Wren::Renderable. | |
# | |
# Wren values separation of concerns and testability so it prefers to | |
# keep templates free of logic by parsing them with Mustache. The plan is | |
# to allow other engines to be used using an adapter. | |
# `response.render` will call `view.render(template_name)` and respond | |
# with the result and the appropriate content type: | |
response.render(view, "say_hello") | |
# Template files will be searched for in `Application#template_dir`, | |
# which defaults to the directory of the application class + | |
# '/templates'. It will look for a file extension that matches the | |
# request format. | |
# | |
# So if the request was for HTML, the above is equivalent to: | |
# response.html(view.render("./templates/say_hello.html")) | |
# Or if the request was for JSON: | |
# response.json(view.render("./templates/say_hello.json")) | |
# | |
# `response.format` is available if you need to perform your own logic | |
# based on the request format. | |
end | |
end | |
class Application | |
# ... | |
end | |
end | |
# What if we want to say hello to other things with a name? As it stands now, | |
# SayHello depends on Person, so it can only say hello to people. We can invert | |
# that dependency (the D in SOLID) by passing in the person to the SayHello | |
# constructor. We can do the same for the view. Wren expects that you will | |
# design your code this way and uses this dependency injection pattern to | |
# enable useful default handlers for things like CRUD on a resource (more on | |
# that later). | |
module Hello | |
Person = Struct.new(:name) | |
class SayHello | |
attr_reader :model, :view | |
def initialize(model:, view:) | |
@model = model | |
@view = view | |
end | |
def call(name:, response:, **) | |
obj = model.new(name) | |
view = view.new(obj) | |
response.render(view, "say_hello") | |
end | |
end | |
class Application | |
extend Wren::Routing | |
extend Wren::Application | |
say_hello = SayHello.new(model: Person, view: Wren::Views::DelegateView) | |
handle get("/hello/:name"), with: say_hello | |
# With the dependencies inverted, SayHello could handle a request for | |
# anything that has a name: | |
# handle get("/hello/:name"), with: SayHello.new(model: Person, view: view) | |
# handle get("/dog/:name"), with: SayHello.new(model: Dog, view: view) | |
# | |
# This is how Wren provides things like Wren::Handlers::CreateResource | |
# without requiring special DSLs for configuring what model is used, etc. | |
# | |
# The general best practice is to let the outter layer of your application | |
# (usually the Application class) provide concrete dependencies and let | |
# your inner classes declare their needed dependencies as required, named | |
# constructor arguments. This lets you easily integration test your full | |
# application stack, and unit test your business logic in isolation | |
# (you can provide mock objects to meet needed dependencies). | |
end | |
end | |
# So far we've only been dealing with one route. What if you have more, and | |
# you want them to link to each other? To reduce duplication of routing | |
# knowledge, assign your routes to constants that can be accessed elsewhere. | |
# For example, imagine we live in a world where we want to say "Hello" *and* | |
# "Goodbye": | |
module MyApp | |
module Routes | |
extend Wren::Routing | |
SayHello = get("/hello/:name") | |
SayGoodbye = get("/goodbye/:name") | |
end | |
module Views | |
class DelegateView < Wren::Views::DelegateView | |
def hello_path | |
Routes::SayHello.path(name: source.name) | |
end | |
def goodbye_path | |
Routes::SayGoodbye.path(name: source.name) | |
end | |
end | |
end | |
class Application | |
extend Wren::Application | |
say_hello = Wren::Handlers::RenderTemplate.new( | |
template: "hello", view: Views::DelegateView) | |
say_goodbye = Wren::Handlers::RenderTemplate.new( | |
template: "goodbye", view: Views::DelegateView) | |
handle Routes::SayHello, with: say_hello | |
handle Routes::SayGoodbye, with: say_goodbye | |
end | |
end | |
# Here, our routes are completely decoupled from their handlers. This | |
# may seem extreme, but I like it. Routes sole responsibility are | |
# representing paths in our project. The application class is responsible | |
# for mapping those routes to handlers. | |
# | |
# There are no global routing helpers. | |
# If you need a piece of the system to understand paths, include or | |
# inject the routes. | |
# | |
# Since our templates are not allowed to contain logic (not even function | |
# calls like SayHello.path(model)), we need to define some path attributes. | |
# This may seem overly verbose, but I like the explicitness. Your template doesn't have | |
# anything available that you are not explicitly providing for it. I | |
# feel that this keeps your templates much cleaner and everything easier | |
# to understand and test. | |
# Our templates will probably look something like this: | |
# | |
# hello.html: | |
# <h1>Hello {{name}}!</h1> | |
# <a href="{{goodbye_path}}">Say goodbye</a> | |
# | |
# goodbye.html: | |
# <h1>Goodbye {{name}}!</h1> | |
# <a href="{{hello_path}}">Come back</a> | |
# The RenderTemplate handler in Wren looks like this: | |
class RenderTemplate | |
attr_reader :template, :view | |
def initialize(template:, view:) | |
@template = template | |
@view = view | |
end | |
def call(request:, response:, **context) | |
obj = OpenStruct.new(context) | |
view = view.new(obj) | |
response.render(template, view) | |
end | |
end | |
# One of the goals of Wren is to keep the framework's core code | |
# as a simple foundation but still help you hit the ground running by providing | |
# things like common RESTful Handler classes that are built on that | |
# foundation. But built the same way you would write your own handlers. | |
# This way the libraries and tests in wren itself can provide documentation | |
# and examples of how you can write your own wren code. | |
# TODO: | |
# * Figure out how routes know their mount path | |
# * Document a typical REST-oriented architecture | |
# * Document built-in Wren handlers for the default RESTful behavior. | |
# Routing ideas: | |
# * Make apps routable and provide a router method? | |
# * How to get routes all the way down to templates? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment