Only this pageAll pages
Powered by GitBook
1 of 19

Azu

AZU

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Configuration

Azu configuration properties are minimal this is to help you keep everything in a mental model without overloading your brain.

Overview

Azu configuration properties are minimal this is to help you keep everything in a mental model without overloading your brain.

/my_app/src/my_app.cr
require "azu"

module MyApp
  include Azu
  
  VERSION = "0.1.0"
  
  configure do
    port = 4000
    host = localhost
    port_reuse = true
    log = Log.for("My Awesome App")
    env = Environment::Development
    template.path = "./templates"
    template.error_path = "./error_template"
  end
end

# Starts the HTTP Server
MyApp.start [
  Azu::Handler::Rescuer.new,
  Azu::Handler::Logger.new,
]

Configuration properties lives within your application's main file.

Configuration Properties

Apps sometimes store config as constants in the code. This is a violation of the twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.

We recommend for development use .env files.

  1. .env file

  2. .env.local file

  3. .env.#{CRYSTAL_ENV} file

  4. .env.#{CRYSTAL_ENV}.local file

Important: Azu does NOT load the .env files automatically. It is up to you the developer to use your preferred method to load environment variables.

Configuration

Environment Variables

Default Value

port

PORT

4000

jj port_reuse

PORT_REUSE

true

host

HOST

"0.0.0.0"

log

CRYSTAL_LOG_LEVEL, CRYSTAL_LOG_SOURCES

Log.for("Azu")

env

CRYSTAL_ENV

Environment::Development

template.path

TEMPLATES_PATH

"./templates"

template.error_path

ERROR_TEMPLATE

"./error_template"

ssl_cert

SSL_CERT

""

ssl_key

SSL_KEY

""

ssl_ca

SSL_MODE

""

ssl_mode

SSL_CA

"none"

Example

export CRYSTAL_ENV=development 
export CRYSTAL_LOG_SOURCES="*" 
export CRYSTAL_LOG_LEVEL=info 
export CRYSTAL_WORKERS=8 
export PORT=4000 
export PORT_REUSE=true 
export HOST=0.0.0.0

App Environments

By default Azu ships with the following environments:

  • Build

  • Development

  • Test

  • Integration

  • Acceptance

  • Pipeline

  • Staging

  • Production

The current application environment is determined via the CRYSTAL_ENV variable.

Use the following method to determine or compare environments

MyApp.env 
# => Production
MyApp.env.production?
# => true
MyApp.env == Environment::Development
# => false
MyApp.env.in? :development
# => false

Middlewares

Finally, you must know how to start the server.

MyApp.start [
  Azu::Handler::Rescuer.new,
  Azu::Handler::Logger.new,
]

Azu follows the standards this is why Azu first attempts to load configuration values from the environment first if not found then it loads from code.

https://12factor.net/config

Create new app

Learn how to get started with Azu and build applications

Let's get a Azu application up and running as quickly as possible.

Generate a Crystal App Project

We can run crystal init app my_app from any directory in order to bootstrap our Azu application. Assuming that the name of our application is my_app, let's run the following command:

❯ crystal init app my_app
    create  /my_app/.gitignore
    create  /my_app/.editorconfig
    create  /my_app/LICENSE
    create  /my_app/README.md
    create  /my_app/.travis.yml
    create  /my_app/shard.yml
    create  /my_app/src/my_app.cr
    create  /my_app/spec/spec_helper.cr
    create  /my_app/spec/my_app_spec.cr
Initialized empty Git repository in /home/eperez/workspaces/my_app/.git/

Add the dependency to your shard.yml

Open your /my_app/shard.yml in your favorite editor and add the azu dependency

dependencies:
  azu:
    github: azutoolkit/azu

Azu is very light, flexible and module, for this reason it does not add front-end dependencies nor database dependencies. We will teach you how you can integrate those later in the guides

Run Shards Install

Now install Azu by running shards install from the terminal

❯ shards install

Enabling Azu in Your project

Now that you have install Azu, lets enable it in your project. Open /my_app/src/my_app.cr

/my_app/src/my_app.cr
require "azu"

# TODO: Write documentation for `MyApp`
module MyApp
  VERSION = "0.1.0"

  # Include the Azu module
  include Azu
end

With the simple include azu you have unlocked the benefits of Azu for your project.

Available methods

Name

MyApp.configure

MyApp.log

This is the application logger and uses Crystal built in logger by default

MyApp.env

Allows you to work with the application current environment Production, Development, Staging, etc.

MyApp.config

Gets your application configuration for easy access from other parts of the code base

MyApp.start

Starts the HTTP server

Before we begin, please take a minute to read the. By installing any necessary dependencies beforehand, we'll be able to get our application up and running smoothly.

Accepts a block and allows you to define Azu

Installation Guide
configuration

Prologue

AZU is a set of tools that offers building blocks to create Crystal Applications requiring little to no boilerplate code making you more efficient and productive.

Azu was created by harvesting, it started by not trying to build a framework, but by building an application. While you build the application you don't try to develop generic code, but you do work hard to build a well-factored and well-designed application.

Benefits

  • Plain Crystal, little to no DSL

  • Small DSL, Plain Old Crystal

  • No magic, no surprises

  • Type-safe definitions

  • Adopts to your architectural pattern

    • Model, View, Controller

    • Modular

    • Pipes and Filters

    • Event-Driven

    • Layered

With one application built you then build another application that has at least some similar needs to the first one. While doing we pay attention to any duplication between the second and first application. As you find duplication you factor out into a common area, this common area is Azu Toolkit

Conceptualization

Azu helps you to have the clarity to separate and represent the Input and Output at every step of the design lifecycle. Azu does this by mapping objects' names to the actual technical names so you start to build a mental model on how the toolkit enables you, at the same time understand the design process.

Mental Model

All applications will have business-specific use cases for which the application has been built, a pattern that we recommend to follow for building any application.

  • Plain Old Crystal Objects (POCO) - Also known as Value Objects are used to pass immutable data around your domain.

  • Use Cases - expect simple request data structures, for its inputs and produces simple response data structures for its output.

Contracts

Requests are designed by contract in order to enforce correctness and type-safe definitions

HTTP Requests

Every HTTP request message has a specific form:

POST /path HTTP/1.1
Host: example.com

foo=bar&baz=bat

An HTTP message is either a request from a client to a server or a response from a server to a client.

Azu Contract Request

Requests are designed by contract in order to enforce correctness. What this means is that requests are strictly typed and can have preconditions. With this concept.

The Request provides concise, type-safe, and self-validated request objects.

struct UserContract
  include Azu::Contract
  
  getter name : String
  validate name, message: "Param name must be present.", presence: true
end

Benefits:

  • Self-documented request objects.

  • Type-safe requests and parameters

  • Enables Focused and effective testing.

  • JSON body requests render object instances.

Example Use:

Request Instances

Requests can be initialized from JSON, YAML, and the standard initializes method new.

UserContract.from_json(pyaload: String)
UserContract.new(params: Hash(String, String))

Instance Methods

Instance Method

Description

validate

A macro to define validation rules for your request

valid?

Validates the request object and returns true or false

validate!

Validates and raises an exception when invalid

rules

returns a list of rules to be applied on validation

params

to_json

A JSON representation of the request

to_yaml

A YAML representation of the request

Custom Validators

When the built-in validation helpers are not enough for your needs, you can write your own validators or validation methods as you prefer.

Defining Custom Validators

Custom validators are simple classes that inherit from Schema::Validator. These classes must implement the valid? method which takes a record as an argument and performs the validation on it. The custom validator is called using the valid? or validate! method.

class ConfirmPasswordValidator < Schema::Validator
  getter :record, :field, :message

  def initialize(@record : Authority::NewOwnerRequest)
    @field = :password
    @message = "Password did not match with confirm password."
  end

  def valid? : Array(Schema::Error)
    if @record.password != @record.confirm_password
      [Schema::Error.new @field, @message]
    else
      [] of Schema::Error
    end
  end
end

Using Custom Validators

To enable the custom validator to your structs or classes by simply defining the use ConfirmPasswordValidator

module Authority
  struct NewOwnerRequest
    include Request

    getter first_name : String = ""
    getter last_name : String = ""
    getter email : String = ""
    getter username : String = ""
    getter password : String = ""
    getter confirm_password : String = ""
    
    ## Use Custom Validator
    use ConfirmPasswordValidator

    validate first_name, message: "Param first_name must be present.", presence: true
    validate last_name, message: "Param last_name must be present.", presence: true
    validate email, message: "Param email must be present.", presence: true
    validate username, message: "Param username must be present.", presence: true
    validate password, message: "Param password must be present.", presence: true
  end
end

Azu::Contract are provided by tight integration with the shard

Requests can be initialized are initialized in the background and property is available to the of the same name of the request as the camel case.

Request params. See

Schema
Endpoint
Params

Response

Responses is mostly an Azu implementation detail to enable more type-safe definitions.

Response is mostly an Azu implementation detail to enable more type-safe definitions, and it does not represent the raw response from the HTTP::Server::Response. Instead responses are plain simple crystal objects that allows for easy implementation, validation and test-ability.

Azu::Responses main job is to render the body of the responses to be sent back to browsers and API clients.

Defining an Azu::Response

Responses are created by including the Azu::Response and defining a render method that contains the body for the HTTP response.

Because of the simplicity of the module it is easy to create type safe responses, that can be validated and easily tested.

For example, lets say we want to render an IndexPage for a dashboard, we want to make ensure that a title is always provided in order to display the page.

Responses are simple Crystal classes that includes the Azu::Response module, there is no magic or macro needed, simply follows crystal conventions.

Rendering Inline HTML

Taking the example IndexPage above we can represent the H1 using Markup DSL

Rendering Templates

Azu::Response main job is to render the body of the responses to be sent back to browsers and API clients. Most of the time, we use templates to build said responses.

Templates work great for many reasons, some of those, easy to share with designers and front-end developers, portable across teams, and clear separation from presentation and back-end code.

To use templates include the Templates::Renderable module and use the render/2 method

Azu provides a module to allow you write HTML in plain crystal

# Response Docs https://azutopia.gitbook.io/azu/endpoints/response
module MyApp
  class UserResponse
    include Response

    def initialize(@name : String)end

    def render
      "Hello, #{@name}!"
    end
  end
end
module MyApp
  class Dashboard::IndexPage
    include Response
    
    def initialize(@title : String)
    end

    def render
      "<h1>#{@title}</h1>"
    end
  end
end
module MyApp
  class Dashboard::IndexPage
    include Response
    
    def initialize(@title : String)
    end

    def render
      h6 @title, class: "mg-b-0"
    end
  end
end
module MyApp
  struct Users::ShowPage
    include Response
    include Templates::Renderable

    TEMPLATE = "users/show.jinja"
    
    getter username : String

    def initialize(@username : String)
    end

    def render
      render template, {"user_name" => username}
    end
  end
end
Markup
Learn more about using Templates

Endpoints

Endpoints is a simple module that defines the resource, and allows you to access the Request object and build the Response.

Endpoints is a simple module that defines the resource, and allows you to access the Request object and build the Response.

The endpoint is the final stage of the request process Each endpoint is the location from which APIs can access the resources of your application to carry out their function.

Endpoints as Contracts

To ensure correctness Endpoints are designed with the Request and Response pattern in mind you can think of it as input and output to a function, where the request is the input and the response is the output.

 include Endpoint(UserRequest, UserResponse)

Request and Response objects are type-safe objects that can be designed by contract.

/my_app/endpoints/user_endpoint.cr
module MyApp
  struct UserRequest
    include Request
  end
  
  class UserEndpoint
    include Endpoint(UserRequest, UserResponse)
    
    get "/users"
  end
end

Creating Endpoints

An Endpoint requires at a minimum a Request and Response Object

/my_app/endpoints/user_endpoint.cr
class UserEndpoint
    include Endpoint(UserRequest, UserResponse)
    
    def call : UserResponse
        # .. your code here ..
    end
end

Basic Methods

The endpoint gives you access to a wide rage of functions to allow you inspect the request and modify the response.

Method

Description

params

context

method

HTTP Request Method

header

Gets all HTTP Headers

json

Gets request body as string

cookies

Gets request cookies

content_type(type : String)

Sets the content type of the response

header(key : String, value : String)

Sets response header

redirect(to location : String, status : Int32 = 301)

Redirect requests to a new resource

cookie(cookie : HTTP::Cookie)

Sets response cookie

status(status : Int32)

Sets response status

error(message : String, status : Int32 = 400, errors = [] of String)

Renders a response error

The call method

/my_app/endpoints/user_endpoint.cr
def call : UserResponse
# .. your code here ..
end

Request Object

struct UserRequest
  include Request
  
  getter name : String
  
  validate name, message: "Param name must be present.", presence: true
end

The endpoint will contain a user_request method that returns an instance of the UserRequest object.

/my_app/endpoints/user_endpoint.cr
class UserEndpoint
  include Endpoint(UserRequest, UserResponse)

  def call : UserResponse
    user_request.name
  end
end

Inline Routing

The most basic Azu routes accept a URI and a HTTP::Handler.

Routes can be defined withing Endpoints. Having routes defined close to the class that uses it allows for code cohesiveness and readability

module MyApp
  class UserEndpoint
    include Endpoint(UserRequest, UserResponse)
    
    get "/users"
  end
end

All HTTP verbs are available as macro methods within Endpoints.

For flexibility Azu allows you to define routes outside of Endpoints.

Route Helpers

Gets raw object from the raw

HTTP Server context. Allows access to the raw Request and objects

Every endpoint must define a call method that returns a object.

A request object encapsulates the structure of the incoming HTTP request as needed per your application, allowing you to inspect, validate and ensuring correctness. Read more about

Read more about

Response
Requests
Requests
params
request
Response

Installation

The only requirement to use Azu is the Crystal Language itself

Bigger applications

In order to build a full-featured Azu application, you will need a few dependencies installed.

  • Shards command line tool for package management (Installed by default with Crystal installation)

  • a database - Azu recommends PostgreSQL but you can pick others or not use a database at all

  • Node.js for assets - which can be opt-out, especially if you are building APIs

Azu Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      azu:
        github: azutoolkit/azu
  2. Run shards install

Summary

the Crystal Programming Language - installation instruction can be found

At the end of this section, you must have installed Crystal, Shards, PostgreSQL and Node.js. Now that we have everything installed, let's.

here
define our first Azu application

Params

Access request parameters from the request body, query string, path

Params, short for parameters, allow you to access data sent by the client with the request. Requests can have parameters in different places, intuitively the params can be accessed by the attribute context location:

Params can come from:

  • Request path (eg. /books/:id)

  • Query string (eg. /books?title=Hanami)

  • Request body (eg. a POST request to /books)

Other than Path and Query parameters, all other parameters are parsed depending on the "Content-Type" header

Access to Parameters

Params provide hash-like access to the request attributes. Since attributes can be expected in different contexts of the request such as path, body, and query, the lookup happens in the following order of precedence.

  1. Form

  2. Path

  3. Query

To access the value of a param, we can use the subscriber operator #[].

Given an Endpoint with a path of "/users/:id"

module MyApp
  struct UserRequest
    include Request
  end
  
  class UserEndpoint
    include Endpoint(UserRequest, UserResponse)
    
    get "/users/:id"
    
    def call : UserResponse
      id = params["id"]
    end
  end
end

If we visit /users/john, we should see path string:john.

JSON Parameters

If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", will automatically be loaded into the params.json object.

So for example, if you are sending this JSON content:

{ "company": { "name": "acme", "address": "123 Carrot Street" } }

The params object will contain a string with the content of the JSON.

Note: Azu does not convert the params.json string into a JSON ANY object, and instead the developer can decide how to best parse.

Understnding your project

When we use crystal init app my_app to generates a new Azu application, it generated an empty Crystal App directory structure.

~/workspaces/my_app master
❯ tree
.
├── LICENSE
├── README.md
├── shard.yml
├── spec
│   ├── my_app_spec.cr
│   └── spec_helper.cr
└── src
    └── my_app.cr

2 directories, 6 files

Many frameworks build a default directory structure to start with. Azu does not implement a default set of conventions, and instead provides you with patterns that fits best your needs.

Model View Controller

~/workspaces/my_app master
❯ tree
.
├── LICENSE
├── README.md
├── shard.yml
├── spec
│   ├── my_app_spec.cr
│   └── spec_helper.cr
└── src
    └── my_app.cr
    └── endpoints
    └── models
    └── views

2 directories, 6 files

Custom Directory Structure

~/workspaces/joobq_gui master
❯ tree src
src
├── channels
│   └── charts_channel.cr
├── components
│   ├── commons.cr
│   ├── duration_counter.cr
│   ├── errors_counter.cr
│   ├── jobs_counter.cr
│   ├── jobs_table.cr
│   ├── latency_counter.cr
│   ├── processing_chart.cr
│   ├── processing_counter.cr
│   └── queues_table.cr
├── config
│   └── joobq.cr
├── dashforge.cr
├── endpoints
│   ├── dashboard.cr
│   ├── jobs.cr
│   ├── queues.cr
│   ├── scheduler.cr
│   └── static.cr
├── jobs
│   ├── email_job.cr
│   ├── fail_job.cr
│   └── test_job.cr
└── pages
    ├── dashboard
    │   └── index_page.cr
    ├── jobs
    │   └── show_page.cr
    ├── queues
    │   ├── show_page.cr
    │   └── traces_page.cr
    └── scheduler
        └── index_page.cr

10 directories, 25 files

Crinja Templates

Azu uses Crinja a powerful template engine built for the Crystal Language

,Templates::Renderable will define a private function named render(template : String, data) with one clause per file system template.

 class Dashboard::IndexPage
    include Response
    include Templates::Renderable
    
    TEMPLATE = "dashboard/index.jinja"
    
    getter joobq = JoobQ.statistics

    def render
      render TEMPLATE, {
        "busy"                  => ProcessingCounter.mount,
        "latency"               => LatencyCounter.mount,
        "counts"                => counts[:busy],
        "processing_chart_data" => {data: processing}.to_json,
        "processing_status"     => ProcessingChart.mount,
        "queue_table"           => QueuesTable.mount,
      }
    end
  end

Template Syntax

The following is a quick overview of the template language to get you started.

Expressions

In a template, expressions inside double curly braces ({{ ... }}) will be evaluated and printed to the template output.

Assuming there is a variable name with value "World", the following template renders Hello, World!.

Hello, {{ name }}!

The properties of an object can be accessed by a dot (.) or square brackets ([]). Filters modify the value of an expression.

Hello, {{ current_user.name | default("World") | titelize }}!

Tests are similar to filters but are used in the context of a boolean expression, for example as a condition of an if tag.

{% if current_user is logged_in %}
  Hello, {{ current_user.name }}!
{% else %}
  Hey, stranger!
{% end %}

Tags

Tags control the logic of the template. They are enclosed in {% and %}.

{% if is_morning %}
  Good Morning, {{ name }}!
{% else %}
  Hello, {{ name }}!
{% end %}

The for tag allows looping over a collection.

{% for name in users %}
  {{ user.name }}
{% endfor %}

Other templates can be included using the include tag:

{% include "header.html" %}

<main>
  Content
</main>

{% include "footer.html" %}

Macros

Macros are similar to functions in other programming languages.

{% macro say_hello(name) %}Hello, {{ name | default("stranger") }}!{% endmacro %}
{{ say_hello('Peter') }}
{{ say_hello('Paul') }}

Template Inheritance

Template inheritance enables the use of block tags in parent templates that can be overwritten by child templates. This is useful for implementing layouts:

{# layout.html #}

<h1>{% block page_title %}{% endblock %}</h1>

<main>
  {% block body %}
    {# This block is typically overwritten by child templates #}
  {% endblock %}
</main>

{% block footer %}
  {% include "footer.html" %}
{% endblock %}
{# page.html #}
{% extends "layout.html" %}

{% block page_title %}Blog Index{% endblock %}
{% block body %}
  <ul>
    {% for article in articles if article.published %}
    <div class="article">
      <li>
        <a href="{{ article.href | escape }}">{{ article.title | escape }}</a>
        written by <a href="{{ article.user.href | escape}}">{{ article.user.username | escape }}</a>
      </li>
    {%- endfor %}
  </ul>
{% endblock %}

More details can be found in . The original can also be helpful, Crinja templates are mostly similar.

the template guide
Jinja2 template reference

Markup

Azu::Markup is a Crystal DSL for HTML that enables writing HTML in plain Crystal, ensuring type-safe HTML with valid syntax, and automatically escapes attribute values. It supports HTML 4 and 5, and XHTML.

Example Usage

p "A paragraph"
# => <p>A paragraph</p>

p do
  text "A paragraph"
end
# => <p>A paragraph</p>

h1 "A first-level heading", class: "heading"
# => <h1 class='heading'>A first-level heading</h1>

h1 class: "heading" do
  text "A first-level heading"
end

# => <h1 class='heading'>A first-level heading</h1>

ul id: "a-wrapper", class: "list-wrap" do
  ["aa", "bb", "cc"].each do |x|
    li x, class: "list-item"
  end
end
#=> <ul id='a-wrapper' class='list-wrap'>
     <li class='list-item'>aa</li>
     <li class='list-item'>bb</li>
     <li class='list-item'>cc</li>
   </ul>

input type: "checkbox", checked: nil
# => HTML 4, 5: <input type='checkbox' checked>
# => XHTML: <input type='checkbox' checked='checked' />

Live Rendering

Spark provides rich, real-time user experiences with server-rendered HTML

Introduction

Spark will re-render the relevant parts of its HTML template and push it to the browser, which updates itself in the most efficient manner. This means developers write Spark templates as any other server-rendered HTML and Spark does the hard work of tracking changes and sending the relevant diffs to the browser.

At the end of the day, a Spark is nothing more than a process that receives events as messages and updates its state. The state itself is nothing more than functional and immutable Crystal data structures.

This architecture eliminates the complexity imposed by full-stack front-end frameworks without abandoning high-performance reactive user experiences. With Spark, small teams can do big things faster than ever before. We invite you to explore a fresh alternative to the Single Page App (SPA).

Server Side Web Socket Connection

To enable Spark live rendering you must first mount the /live-view. In your main application crystal file simple add the following snippet.

module MyApp
  include Azu
  
  # Defines a Spark websocket connection
  router.ws "/live-view", Spark.new
end

With this snippet we define the the Server side Web Socket connection will be used to render all your spark components.

Client Side Web Socket Connection

In order for Spark to communicate with the Browser we must define a Web Socket connection on the Client side. This is done by including the spark javascript.

<script src="/assets/js/live-view.js" type="module"></script>

Spark Components Overview

Spark Components decompose response content into small independent contexts that can be lazily loaded.

Defining Components

Components are defined by including Azu::Component and are mounted/rendered by calling YourComponent.mount in a parent Azu::Response. Components run inside the Response object, but may have their own state and event handling.

Example Spark Component

class TitleComponent
  include Azu::Component

  def initialize(@name : String)
  end

  def mount
    every(5.seconds) { refresh }
  end

  def content
    h1 { random_name_generator }
  end
  
  private def random_name_generator
    ["John", "Doe", "Karen"].sample.first
  end
end

The Mount Method

The most basic way to render a Spark component on a page is using the component mount method

You begin by rendering a Spark component typically from your Response class. A Spark component's mount method gets called on the initial page load and every subsequent component update.

Example Azu Response rendering a Spark Component

class Dashboard::IndexPage
  include Response
  include Template::Renderable
  
  TEMPLATE = "templates/layout.jinja"
  
  def render
    render TEMPLATE, { "title": TitleComponent.mount }
  end
end

Initializing Properties

A Spark component can be initialize with properties using the mount method. For this to work simply define an initialize method with the name properties as you would normally do with any Crystal class.

The use of properties enables re-usability of the components create.

class ProcessingCounter
  include Azu::Component
  
  # Spark Component Properties
  def initialize(@title : String, @count : Int32)
  end
  
  def mount
    every(1.seconds) { refresh }
  end

  def content
    h6 @title, class: "mg-b-0"
    text @count
  end
end

Mounting the component above would look like

ProcessingCounter.mount(title: "Total Jobs", count: 100)

Refreshing the component

Spark components comes with the refresh and every methods and allows you to define the refresh interval for a given component

def mount
  every(5.seconds) { refresh }
end

Component Content

With every Spark Component you must define a content method. The body of the method it's what gets rendered when the component is mounted to the page.

Spark components are meant to render dynamic content, at the same time components must be reusable and easy to maintain, for these reason Spark components have Markup, a lean html dsl written in Crystal to allow you write HTML natively.

Markup is intuitive to use and fast to render.

def content
  div class: "card" do
    div class: "card-header" do
      h6 "Processing", class: "mg-b-0"
    end

    div class: "card-body tx-center" do
      h4 class: "tx-normal tx-rubik tx-40 tx-spacing--1 mg-b-0" do
        text processing_count
        small " jobs"
      end

      div "Jobs currently busy", class: "divider-text"
    end
  end
end

While you can use Markup to write your html code is not limited. You can use docstrings, templates or what suits your needs bests.

Templating

Getting Started

Internationalization (I18n)

Crystal I18n is an internationalization library for the Crystal programming language a unified interface allowing to leverage translations and localized contents in a Crystal project.

Installation

Simply add the following entry to your project's shard.yml:

dependencies:
  i18n:
    github: crystal-i18n/i18

And run shards install afterwards.

Quick usage

Assuming that a config/locales relative folder exists in your project, with the following en.yml file in it:

en:
  simple:
    translation: "This is a simple translation"
    interpolation: "Hello, %{name}!"
    pluralization:
      one: "One item"
      other: "%{count} items"

The following setup could be performed in order to initialize I18n properly:

require "i18n"

I18n.config.loaders << I18n::Loader::YAML.new("config/locales")
I18n.config.default_locale = :en
I18n.init

Here a translation loader is configured to load the previous translation file while also configuring the default locale (en) and initializing the I18n module.

Translations lookups can now be performed using the #translate method (or the shorter version #t) as follows:

I18n.t("simple.translation")                     # outputs "This is a simple translation"
I18n.t("simple.interpolation", name: "John Doe") # outputs "Hello, John Doe!"
I18n.t("simple.pluralization", count: 42)        # outputs "42 items"

Read More About the I18n Shard

Mailers

Start sending and receiving emails from and to your Crystal application

Installation

Add this to your application's shard.yml:

dependencies:
  carbon:
    github: luckyframework/carbon

Adapters

Usage

First, create a base class for your emails

require "carbon"

# You can setup defaults in this class
abstract class BaseEmail < Carbon::Email
  # For example, set up a default 'from' address
  from Carbon::Address.new("My App Name", "support@myapp.com")
  # Use a string if you just need the email address
  from "support@myapp.com"
end

Configure the mailer class

BaseEmail.configure do |settings|
  settings.adapter = Carbon::DevAdapter.new(print_emails: true)
end

Create a class for your email

# Create an email class
class WelcomeEmail < BaseEmail
  def initialize(@name : String, @email_address : String)
  end

  to @email_address
  subject "Welcome, #{@name}!"
  header "My-Custom-Header", "header-value"
  reply_to "no-reply@noreply.com"
  # You can also do just `text` or `html` if you don't want both
  templates text, html
end

Create templates

Templates go in the same folder the email is in:

  • Text email: <folder_email_class_is_in>/templates/<underscored_class_name>/text.ecr

  • HTML email: <folder_email_class_is_in>/templates/<underscored_class_name>/html.ecr

So if your email class is in src/emails/welcome_email.cr, then your templates would go in src/emails/templates/welcome_email/text|html.ecr.

# in <folder_of_email_class>/templates/welcome_email/text.ecr
# Templates have access to instance variables and methods in the email.
Welcome, <%= @name %>!
# in <folder_of_email_class>/templates/welcome_email/html.ecr
<h1>Welcome, <%= @name %>!</h1>

Template layouts

Layouts are optional allowing you to specify how each email template looks individually. If you'd like to have the same layout on each, you can create a layout template in <folder_email_class_is_in>/templates/<layout_name>/layout.ecr

In this file, you'll yield the main email body with <%= content %>. Then in your BaseEmail, you can specify the name of the layout.

abstract class BaseEmail < Carbon::Email
  macro inherited
    from default_from
    layout :application_layout
  end
end
# in src/emails/templates/application_layout/layout.ecr

<h1>Our Email</h1>

<%= content %>

<div>footer</div>

Deliver the email

# Send the email right away!
WelcomeEmail.new("Kate", "kate@example.com").deliver

# Send the email in the background using `spawn`
WelcomeEmail.new("Kate", "kate@example.com").deliver_later

Delay email delivery

The built-in delay uses the deliver_later_strategy setting set to Carbon::SpawnStrategy. You can create your own custom delayed strategy that inherits from Carbon::DeliverLaterStrategy and defines a run method that takes a Carbon::Email and a block.

One example might be a job processor:

# Define your new delayed strategy
class SendEmailInJobStrategy < Carbon::DeliverLaterStrategy

  # `block.call` will run `deliver`, but you can call
  # `deliver` yourself on the `email` when you need.
  def run(email : Carbon::Email, &block)
    EmailJob.perform_later(email)
  end
end

class EmailJob < JobProcessor
  def perform(email : Carbon::Email)
    email.deliver
  end
end

# configure to use your new delayed strategy
BaseEmail.configure do |settings|
  settings.deliver_later_strategy = SendEmailInJobStrategy.new
end

Testing

Change the adapter

# In spec/spec_helper.cr or wherever you configure your code
BaseEmail.configure do
  # This adapter will capture all emails in memory
  settings.adapter = Carbon::DevAdapter.new
end

Reset emails before each spec and include expectations

# In spec/spec_helper.cr

# This gives you the `be_delivered` expectation
include Carbon::Expectations

Spec.before_each do
  Carbon::DevAdapter.reset
end

Integration testing

# Let's say we have a class that signs the user up and sends the welcome email
# that was described at the beginning of the README
class SignUpUser
  def initialize(@name : String, @email_address : String)
  end

  def run
    sign_user_up
    WelcomeEmail.new(name: @name, email_address: @email_address).deliver
  end
end

it "sends an email after the user signs up" do
  SignUpUser.new(name: "Emily", email_address: "em@gmail.com").run

  # Test that this email was sent
  WelcomeEmail.new(name: "Emily", email_address: "em@gmail.com").should be_delivered
end

# or we can just check that some emails were sent
it "sends some emails" do
  SignUpUser.new(name: "Emily", email_address: "em@gmail.com").run

  Carbon.should have_delivered_emails
end

Unit testing

Unit testing is simple. Instantiate your email and test the fields you care about.

it "builds a nice welcome email" do
  email = WelcomeEmail.new(name: "David", email_address: "david@gmail.com")
  # Note that recipients are converted to an array of Carbon::Address
  # So if you use a string value for the `to` field, you'll get an array of
  # Carbon::Address instead.
  email.to.should eq [Carbon::Address.new("david@gmail.com")]
  email.text_body.should contain "Welcome"
  email.html_body.should contain "Welcome"
end

Note that unit testing can be superfluous in most cases. Instead, try unit testing just fields that have complex logic. The compiler will catch most other issues.

Azu leverages the library for writing, sending, and testing emails. Carbon can be configured using the default file generated with a new Azu application in initializers/carbon.cr. In that file, you can add SendGrid keys and change adapters.

Carbon::SendGridAdapter- See .

Carbon::SmtpAdapter - See .

Carbon::AwsSesAdapter - See .

Carbon::SendInBlueAdapter - See .

Carbon::MailgunAdapter - See .

Carbon::SparkPostAdapter - See .

Carbon::PostmarkAdapter - See .

Carbon::MailersendAdapter - See .

For more information on what you can do with Embedded Crystal (ECR), see .

Carbon
luckyframework/carbon_sendgrid_adapter
luckyframework/carbon_smtp_adapter
keizo3/carbon_aws_ses_adapter
atnos/carbon_send_in_blue_adapter
atnos/carbon_mailgun_adapter
Swiss-Crystal/carbon_sparkpost_adapter
makisu/carbon_postmark_adapter
balakhorvathnorbert/carbon_mailersend_adapter
the official Crystal documentation
Crystal I18n

Validations

Validation in Azu is support by the Schema shards and is already included with every Crystal Application

Self validating Schemas are beneficial, and in my opinion, ideal, for when defining API Requests, Web Forms, JSON. Schema-Validation Takes a different approach and focuses a lot on explicitness, clarity, and precision of validation logic. It is designed to work with any data input, whether it’s a simple hash, an array or a complex object with deeply nested data.

Each validation is encapsulated by a simple, stateless predicate that receives some input and returns either true or false. Those predicates are encapsulated by rules which can be composed together using predicate logic, meaning you can use the familiar logic operators to build up a validation schema.

Usage

include Schema::Validation

Schema instance methods

valid?    - Bool
validate! - True or Raise ValidationError
errors    - Errors(T, S)

Validations

You can also perform validations for existing objects without the use of Schemas.

class User < Model
  include Schema::Validation

  property email : String
  property name : String
  property age : Int32
  property alive : Bool
  property childrens : Array(String)
  property childrens_ages : Array(Int32)

  # To use a custom validator. UniqueRecordValidator will be initialized with an `User` instance
  use UniqueRecordValidator

  # Use the `custom` class name predicate as follow
  validate email, match: /\w+@\w+\.\w{2,3}/, message: "Email must be valid!", unique_record: true
  validate name, size: (1..20)
  validate age, gte: 18, lte: 25, message: "Must be 24 and 30 years old"
  validate alive, eq: true
  
  # Define your custom predicates
  predicates do
    def some?(value : String, some) : Bool
      (!value.nil? && value != "") && !some.nil?
    end

    def if?(value : Array(Int32), bool : Bool) : Bool
      !bool
    end
  end

  def initialize(@email, @name, @age, @alive, @childrens, @childrens_ages)
  end
end

Custom Validations

Simply create a class {Name}Validator with the following signature:

class EmailValidator < Schema::Validator
  getter :record, :field, :message

  def initialize(@record : UserModel)
    @field = :email
    @message = "Email must be valid!"
  end

  def valid? : Array(Schema::Error)
    [] of Schema::Error
  end
end

class UniqueRecordValidator < Schema::Validator
  getter :record, :field, :message

  def initialize(@record : UserModel)
    @field = :email
    @message = "Record must be unique!"
  end

  def valid? : Array(Schema::Error)
    [] of Schema::Error
  end
end

Defining Predicates

You can define your custom predicates by simply creating a custom validator or creating methods in the Schema::Predicates module ending with ? and it should return a boolean. For example:

class User < Model
  property email : String
  property name : String
  property age : Int32
  property alive : Bool
  property childrens : Array(String)
  property childrens_ages : Array(Int32)

  ...

  # Uses a `presense` predicate
  validate password : String, presence: true

  # Use the `predicates` macro to define predicate methods
  predicates do
    # Presence Predicate Definition
    def presence?(password : String, _other : String) : Bool
      !value.nil?
    end
  end

  def initialize(@email, @name, @age, @alive, @childrens, @childrens_ages)
  end
end

Differences: Custom Validator vs Predicates

The differences between a custom validator and a method predicate are:

Custom Validators

  • Must be inherited from Schema::Validator abstract

  • Receives an instance of the object as a record instance var.

  • Must have a :field and :message defined.

  • Must implement a def valid? : Array(Schema::Error) method.

Predicates

  • Assertions of the property value against an expected value.

  • Predicates are light weight boolean methods.

  • Predicates methods must be defined as def {predicate}?(property_value, expected_value) : Bool .

Built in Predicates

These are the current available predicates.

gte   - Greater Than or Equal To
lte   - Less Than or Equal To
gt    - Greater Than
lt    - Less Than
size  - Size
in    - Inclusion
regex - Regular Expression
eq    - Equal

Additional Parameters

message - Error message to display
nilable - Allow nil, true or false

CONTRIBUTE - Add more predicates to this shards by contributing a .

Pull Request

Session Management

This chapter describes some particular attacks related to sessions, and security measures to protect your session data.

What are sessions?

HTTP is a stateless protocol, and by default, HTTP requests are independent messages that don't retain user values. However, Session shard implements several approaches to bind and store user state data between requests.

Sessions enable the application to maintain user-specific state, while users interact with the application. For example, sessions allow users to authenticate once and remain signed in for future requests.

Session

Azu offers strongly typed Session Management to manage application sessions and state.

Installation

  1. Add the dependency to your shard.yml:

    dependencies:
      session:
        github: azutoolkit/session
  2. Run shards install

Configuration

require "session"

Session.configure do |c|
  c.timeout = 1.hour
  c.session_key = "_session"
  s.secret = "Secret key for encryption"
  c.on_started = ->(sid : String, data : Databag) { puts "Session started - #{sid}" }
  c.on_deleted = ->(sid : String, data : Databag) { puts "Session Revoke - #{sid}" }
end

Session Stores

The Session shard uses a store maintained by the app to persist data across requests from a client. The session data is backed by a cache and considered ephemeral data.

Recommendation: The site should continue to function without the session data. Critical application data should be stored in the user database and cached in session only as a performance optimization.

The Session shard ships with three forms of session storage out of the box; CookieStore, MemoryStore, and RedisStore.

Cookie Store

The CookieStore is based on a Verifier and Encryptor, which encrypts and signs each cookie to ensure it can't be read or tampered with.

Since this store uses crypto features, you must set the secret field in the configuration.

Session.configure do |c|
  ...
  s.secret = "Secret key for encryption"
  ...
end

After the secret is defined, you can instantiate the CookieStore provider

module MyAzuApp
  class_getter session = Session::CookieStore(UserSession).provider
end

Memory Store

The memory store uses server memory and is the default for the session configuration.

We don't recommend using this store in production. Every session will be stored in MEMORY, and the shard will not remove session entries upon expiration unless you create a task responsible for cleaning up expired entries.

Also, multiple servers cannot share the stored sessions.

module MyAzuApp
  class_getter session = Session::MemoryStore(UserSession).provider
end

Redis Store

The RedisStore is recommended for production use as it is highly scalable and is shareable across multiple processes.

module MyAzuApp
  class_getter session = Session::RedisStore(UserSession).provider(client: Redis.new)
end

Accessing Session Data

The Session shard offers type-safe access to the values stored in the session, meaning that to store values in the session, you must first define the object.

Session Data Object

To define a session data object

# Type safe session contents
struct UserSession
  include Session::Databag
  property username : String? = "example"
end

To write and read to and from the current_session

MyApp.session.data.username # Reads the value of the username property
MyApp.session.data.username = "Dark Vader" # Sets the value of the username property

The Session API

MyApp.session.create           # Creates a new session
MyApp.session.storage          # Storage Type RedisStore or MemoryStore
MyApp.session.load_from        # Loads session from Cookie
MyApp.session.current_session  # Returns the current session
MyApp.session.session_id       # Returns the current session id
MyApp.session.delete           # Deletes the current session
MyApp.session.valid?           # Returns true if session has not expired
MyApp.session.cookie           # Returns a session cookie that can be sent to clients
MyApp.session[]                # Gets session by Session Id or raises an exception
MyApp.session[]?               # Gets session by Session Id or returns nil
MyApp.session.clear            # Removes all the sessions from store

Note: Session also offers a HTTP Handler Session::SessionHandler to automatically enable session management for the Application. Each request that passes through the Session Handlers resets the timeout for the cookie

Session HTTP Handler

A very simple HTTP handler enables session management for an HTTP application that writes and reads session cookies.

module Session
  class SessionHandler
    include HTTP::Handler

    def initialize(@session : Session::Provider)
    end

    def call(context : HTTP::Server::Context)
      @session.load_from context.request.cookies
      call_next(context)
      @session.set_cookies context.response.cookies
    end
  end
end
Logo
GitHub - azutoolkit/session: Session Management LibraryGitHub
Logo