Ignore

Please note that your browser is not supported.

We recommend upgrading to the latest Firefox or Google Chrome.

Reflection: Endpoints

Endpoints are responsible for exposing the dynamic data that is ultimately presented in view templates. Data is pulled directly from data sources, which were discussed in the previous guide. Reflection defines backend endpoints for your application based on the data bindings it finds in your frontend.

Let's look at an example of how an endpoint is defined. Here's a view template that defines a message data binding that presents a single value for content:

frontend/pages/index.html
<article binding="message">
  <p binding="content">
    message content goes here
  </p>
</article>

Reflection defines a backend endpoint for this view template, which looks like this:

Endpoint Name  HTTP Method  Request Path  Who Defined the Endpoint
-------------  -----------  ------------  ------------------------
:root          GET          /             pakyow/reflection

When a request is made to GET /, the reflected endpoint will expose the correct data. You can see exactly what reflection is doing by looking at the logs. Here's what you would see for the above endpoint:

185.00μs http.9b0296e9 | GET / (for 127.0.0.1 at 2019-07-16 18:43:44 +0000)
  1.39ms http.9b0296e9 | [reflection] exposing dataset for `message': #<Pakyow::Data::Proxy:70221658905980 @source=#<Sequel::SQLite::Dataset: "SELECT * FROM `messages` ORDER BY `created_at` DESC">>
  9.53ms http.9b0296e9 | 200 (OK)

Endpoints are created in a controller on the backend. Controllers define routes that call backend behavior for http requests that match a specific method and path. In this case, here's what the controller would look like:

controller do
  get :root, "/" do
    # Reflected behavior is performed here.
  end
end

If we were to write the controller ourselves, here's what it would look like with the behavior filled in:

controller do
  get :root, "/" do
    expose "message", data.messages.all

    render "/"
  end
end

Reflection keeps us from having to write code like this ourselves. Not only that, the reflection automatically updates its behavior to match changes in the view template. You can add more bindings to the template and data will be exposed for them without having to make any changes yourself. Reflection makes your backend reactive to changes in frontend view templates, rather than hardcoded to work with particular set of bindings. This reduces the amount of coordination you would otherwise need to handle yourself, shortening the feedback loop between changes in the process.

Building a resource-based view structure

Reflection makes some decisions about how to structure backend endpoints based on the structure found in your view templates. One of the conventions it relies on is REST, or Representational State Transfer.

REST defines conventions for http apis based around resources. In Reflection, a RESTful resource is defined when an endpoint will interact directly with a data source. Consider the following view template:

frontend/pages/messages/index.html
<article binding="message">
  <p binding="content">
    message content goes here
  </p>
</article>

If you recall from the previous guide, Pakyow defines a messages data source that matches the message binding from the view template. Because the view template is defined within the resource path (messages/index.html), Reflection defines the endpoint for this view within a messages resource.

Resources are just controllers that implement a RESTful api. The messages/index.html page is tied to the list endpoint in the messages resource. This endpoint is responsible for listing all of the messages contained by a resource. In practical terms this means that all the messages in the database will be presented by the messages/index.html view.

REST defines several other endpoints that reflection makes use of. Here's a list, along with what view templates the endpoint is mapped to:

  • list: This endpoint is responsible for presenting a list of objects for a resource. The request path is structured as GET /messages. Reflection maps view templates like messages/index.html to this endpoint.
  • show: This endpoint is responsible for presenting a specific object within a resource. The request path is structured as GET /messages/:id, where :id is the unique id tied to the object in the database. Reflection maps view templates like messages/show.html to this endpoint.
  • new: This endpoint is responsible for presenting the form for a new object. The request path is structured as GET /messages/new. Reflection maps view templates like messages/new.html to this endpoints.
  • edit: This endpoint is responsible for presenting the form for a specific object within a resource. The request path is structured as GET /messages/:id/edit, where :id is the unique id tied to the object in the database. Reflection maps view templates like messages/edit.html to this endpoint.

Nested resources

REST allows for nested resources in cases where one resource is related to another. Reflection uses this to handle associations between data sources. For example, if you wanted to present a list of replies for a specific message, you would define a view template like this:

frontend/pages/messages/replies/index.html
<h1>
  Replies to your message:
</h1>

<article binding="reply">
  <p binding="content">
    reply content goes here
  </p>
</article>

Reflection renders this view with a list of replies for a specific message. The request path would be structured as GET /messages/:message_id/replies, where :message_id is the id of the specific object in the database.

Nested resources can also contain endpoints for show, new, and edit just like top level resources.

Presenting a specific object

Reflected endpoints will always expose a full dataset for presentation, except in cases where more context is available. For example, in the case of resource show endpoints, reflection reduces the dataset down to the specific object being shown.

Here's an example view template, defined at the show path for the message resource:

frontend/pages/messages/show.html
<article binding="message">
  <p binding="content">
    message content goes here
  </p>
</article>

Instead of exposing all the message data, the show endpoint for this view template will present a specific object. For example, if the request path was to /messages/1, reflection would present the message with an id of 1 in the database. If the object doesn't exist, the 404 (Not Found) page would be presented instead.

Reflection always chooses to present the specific object whenever additional context is available for the binding type. This is the case for edit endpoints, as well as endpoints for nested resources.

Presenting nested data

Reflection will automatically include and present nested data when it can. For example, say you have a view template that presents messages along with replies for each message. Here's what the view template might look like:

frontend/pages/index.html
<article binding="message">
  <p binding="content">
    message content goes here
  </p>

  <ul>
    <li binding="reply">
      <p binding="content">
        reply content goes here
      </p>
    </li>
  </ul>
</article>

Reflection automatically creates a replies data source that's associated with the messages data source. The related endpoint will expose the messages that exist, along with any replies for each message. All of the data is presented together, resulting in a rendered view that looks something like this:

<article>
  <p>
    message 1
  </p>

  <ul>
    <li binding="reply">
      <p binding="content">
        first reply to message 1
      </p>
    </li>
  </ul>
</article>

<article>
  <p>
    message 2
  </p>

  <ul>
    <li binding="reply">
      <p binding="content">
        first reply to message 2
      </p>
    </li>

    <li binding="reply">
      <p binding="content">
        second reply to message 2
      </p>
    </li>
  </ul>
</article>

<article>
  <p>
    message 3
  </p>

  <ul>
  </ul>
</article>

Notice that Message 3 doesn't have any replies. This is because no replies had been created for the message in the database. You can handle this use-case more elegantly using an empty version for the reply binding:

frontend/pages/index.html
<article binding="message">
  <p binding="content">
    message content goes here
  </p>

  <ul>
    <li binding="reply">
      <p binding="content">
        reply content goes here
      </p>
    </li>

    <li binding="reply" version="empty">
      no replies to this message yet
    </li>
  </ul>
</article>

Now the rendered view would look something like this:

<article>
  <p>
    message 1
  </p>

  <ul>
    <li binding="reply">
      <p binding="content">
        first reply to message 1
      </p>
    </li>
  </ul>
</article>

<article>
  <p>
    message 2
  </p>

  <ul>
    <li binding="reply">
      <p binding="content">
        first reply to message 2
      </p>
    </li>

    <li binding="reply">
      <p binding="content">
        second reply to message 2
      </p>
    </li>
  </ul>
</article>

<article>
  <p>
    message 3
  </p>

  <ul>
    <li>
      no replies to this message yet
    </li>
  </ul>
</article>

Presenting to multiple bindings of a single type

Sometimes you might want to present the same type of data twice on the same page. For example, maybe you want to present two lists of messages: one for the most recent posts and another ordered alphabetically. Pakyow gives you a way to accomplish this with a feature called binding channels.

Here's what the view template might look like for a use-case like this:

<h1>
  Most recent messages:
</h1>

<article binding="message:recent">
  <p binding="content">
    message content goes here
  </p>
</article>

<h1>
  Messages in alphabetical order:
</h1>

<article binding="message:alphabetical">
  <p binding="content">
    message content goes here
  </p>
</article>

Each message binding is defined with a channel, respectively named recent and alphabetical. In the next section we'll learn how to present different datasets for each of these bindings.

Presenting a custom dataset

Reflection lets you define custom datasets for bindings. This is useful when you want to override the default dataset, which is either all the data for a binding or, in the case of show endpoints, a specific object for the binding.

Building on the example from the previous section, we can specify the dataset to use for each instance of the binding. We'll present a dataset with recent messages in the first binding, and an alphabetically ordered dataset in the second binding.

Here's what the view template looks like with defined datasets:

<h1>
  Most recent messages:
</h1>

<article binding="message:recent" dataset="query: all; order: created_at(desc)">
  <p binding="content">
    message content goes here
  </p>
</article>

<h1>
  Messages in alphabetical order:
</h1>

<article binding="message:alphabetical" dataset="query: all; order: content(asc)">
  <p binding="content">
    message content goes here
  </p>
</article>

Each dataset defines two aspects: 1) the query to perform and 2) how to order results. There's also third option for limiting the dataset. We'll cover each of these more below.

Dataset queries

Queries are defined on the data source that matches the binding. All data sources define an all query by default that returns all of the results from the database. You can define additional queries by defining it on the data source:

source :messages do
  def important
    where(build("content like ?", "sos"))
  end
end

This query returns results that contain "sos" somewhere in their content. The new important query can be used in a from template like this:

<h1>
  Important messages:
</h1>

<article binding="message" dataset="query: important">
  <p binding="content">
    message content goes here
  </p>
</article>
  • Read more about queries for backend data sources →

Dataset ordering

Datasets can be ordered by any of their attributes, in ascending or descending order. Every aspect of ordering can be expressed in the view template. For example, here's how you would order a dataset by created_at date availbale on every data source:

<article binding="message" dataset="order: created_at">
  <p binding="content">
    message content goes here
  </p>
</article>

By default, datasets are ordered in ascending order. If you want to put the most recent messages at the top, you would specify descending order in the view template:

<article binding="message" dataset="order: created_at(desc)">
  <p binding="content">
    message content goes here
  </p>
</article>

Datasets can also be ordered by multiple attributes:

<article binding="message" dataset="order: created_at(desc), content">
  <p binding="content">
    message content goes here
  </p>
</article>

Here the dataset will first be ordered by creation date (most recent first), then alphabetically based on the content of each message.

Dataset limiting

Datasets can be limited to a specific number of results using the limit keyword:

<article binding="message" dataset="limit: 10">
  <p binding="content">
    message content goes here
  </p>
</article>

Here, only 10 messages will be presented in this view template.

Customizing reflected endpoints

Reflection is designed to integrate with any existing backend endpoints that might exist in your project. It extends your application with endpoints only when the doesn't already exist. This gives you a way to replace a reflected endpoint entirely, or extend it to fit your specific needs.

Replacing an endpoint

You can replace a reflected endpoint by defining it yourself. Reflection does not require you to setup your backend endpoints in any particular structure, instead it just looks for a route that matches the request path for the endpoint.

Let's look at an example. In this case, we'll define an endpoint for presenting messages at the root path in our application. Endpoints are defined within controllers, like this:

backend/controllers/default.rb
controller "/" do
  default do
    # Custom behavior can be performed here.
  end
end

The "/" argument defines the request path that the controller is mounted at. Within the controller is a default route that matches requests to GET /. Defining the controller with a default route is all that's required to replace the reflected behavior. Reflection will see that an endpoint already exists and will step aside and let the custom code handle requests.

Extending an endpoint with operations

Endpoints can be extended rather than completely replaced. There are a couple of ways to do this depending on what exactly you need to do. The first approach is to extend the reflect operation.

Operations are a backend feature in Pakyow that let you define a sequence of steps to perform in order. Most of the business logic for an application will be defined in operations, and reflection is no exception. There's only a single step in the reflect operation that pertains to endpoints:

  • expose: Exposes datasets for every binding in the related view template.

Operations can be extended at runtime, injecting custom behavior where you need it. Each step is defined as an "action" on the operation. Here's how you can extend the reflect operation to replace the expose step:

controller "/" do
  default do
    operations.reflect do
      action :expose do
        # Custom behavior goes here.
      end
    end
  end
end

Custom behavior you define in the new expose action will be used instead of the default behavior defined by reflection.

You can also insert new steps into the operation. Here's how you can add behavior before the expose step:

controller "/" do
  default do
    operations.reflect do
      action :custom, before: :expose do
        # Custom behavior goes here.
      end
    end
  end
end

Now the entire reflect operation will be called, but your custom behavior will be called before the built-in expose behavior.

Extending an action with helpers

Reflection makes several helper methods available for you to use. You can use these helpers as primitives to build your own custom behavior with. Here's a list of available endpoint helpers:

with_reflected_endpoint

Lets you safely perform behavior if a reflected endpoint is available.

with_reflected_endpoint do |endpoint|
  # only called if a reflected endpoint is available
end

reflective_expose

Exposes a dataset for every binding in the view template.

Next Up: Actions