線路

senro

a web framework for kanso. write the parts of the app that are yours; conventions place the rest while it compiles.

examples/guestbook/migrations.kso
import "../../models"

# The guestbook's schema, as the changes that built it, oldest first. The
# models derivation folds these into the schema the app is compiled against.
pub all = [
  signed
  joined
  emailed
  confirmable
  counted
  lockable
  tracked
  arrived
  arrived_before
  arrived_from
  arrived_from_before
  pictured
]

signed = models/create_table "signatures" [id name message]

id = models/column "id" "serial"

joined = models/create_table "accounts" [id name digest]

# The address a reset link is sent to.
emailed = models/add_column "accounts" email

# When the address was confirmed, or empty until it is.
confirmable = models/add_column "accounts" confirmed

# How many wrong passwords in a row the account has had, and when too many
# of them locked it, or empty while it is open.
counted = models/add_column "accounts" failures

lockable = models/add_column "accounts" locked

# How many times the account has signed in, and when and from where the
# latest sign-in and the one before it came, as Devise's trackable keeps them.
tracked = models/add_column "accounts" sign_in_count

arrived = models/add_column "accounts" current_sign_in_at

arrived_before = models/add_column "accounts" last_sign_in_at

arrived_from = models/add_column "accounts" current_sign_in_ip

arrived_from_before = models/add_column "accounts" last_sign_in_ip

# The key of the photo a signer chose, or empty for an entry without one.
pictured = models/add_column "signatures" photo

name = models/column "name" "text"

message = models/column "message" "text"

digest = models/column "digest" "text"

email = models/column "email" "text"

confirmed = models/column "confirmed" "text"

failures = models/column "failures" "integer not null default 0"

locked = models/column "locked" "text not null default ''"

sign_in_count = models/column "sign_in_count" "integer not null default 0"

current_sign_in_at = models/column "current_sign_in_at" blank_text

last_sign_in_at = models/column "last_sign_in_at" blank_text

current_sign_in_ip = models/column "current_sign_in_ip" blank_text

last_sign_in_ip = models/column "last_sign_in_ip" blank_text

photo = models/column "photo" blank_text

blank_text = "text not null default ''"
the name
senro (線路) is japanese for a railway line: the track a train runs on. Rails gave web development its conventions; senro lays the same track for a language whose compiler can check them.
an app

a guestbook, whole

senro's first app is a guestbook: visitors make an account, sign in, sign the book and remove what they signed. These are its own files. The panel above is its schema.

A model is a module named for its table, holding the rules for the table's records. senro declares the record itself, signature, from the table. The policies say who may create and remove a signature, and problems says what makes one unfit to keep.

examples/guestbook/signatures.kso
import "../../models"
import "std/text"

# A signature is a row of the `signatures` table. This module is named for
# the table, so senro/models declares `signature` here with a field for each
# column, and writes `signature_from`, which builds one from a row.
#
# Only the person who signed an entry may remove it. The models derivation
# reads this policy and writes `signature_destroyed`, which asks it before it
# deletes anything, so there is no other way to remove a signature.
pub fn destroy? viewer s
  s.name == viewer

# Anyone may sign the book.
pub fn create? _ _
  true

# A signature needs a name and a message, and a message short enough to
# read at a glance.
pub fn problems s
  models/problems [
    (models/rule "name" (text/trim s.name == "") "Say who you are.")
    (models/rule "message" (text/trim s.message == "") "Say something.")
    (models/rule "message" (length s.message > 280) "Keep it under 280.")
  ]

An action is a public function, and its name places it. entries/index answers GET /entries and entries/create answers POST /entries. Nothing else declares a route.

examples/guestbook/entries.kso
import "../../models"
import "../../pg"
import "../../web"
import "./removal"
import "./signatures"
import "./views"
import "std/list"

# The session says who is signed in, and carries a notice across the
# redirect that follows a signature or a sign-in; the page that shows the
# notice clears it.
pub fn index req db
  s = web/session req secret
  pg/query db newest [] .> (r -> shown req r.rows s (filled "" {}) 200)

# A development secret. A deployed guestbook signs its sessions with its own.
pub secret = "the guestbook's development secret"

# A signature is made through its model, whose rules decide whether it is
# fit to keep. It is signed with the name of the account signed in. The
# fields go to Postgres as parameters, so what a visitor types is stored as
# text and never read as SQL.
pub fn create req db
  viewer = web/viewer req secret
  f = put (web/form req.body) "name" viewer
  models/created viewer db signatures/signature_model f
    .> (w -> signed req db w)

A view is a kata template. It names what it takes on its first line, and the compiler checks every field it reads against the record it is handed.

examples/guestbook/views/signed.kata
takes signatures form flash removals viewer
header
  h1 Guestbook
  p.lede Leave a line for whoever comes next.
if viewer == ""
  nav.door
    a href="/session/new" Sign in
    a href="/account/new" Make an account
else
  form.out method="post" action="/session"
    input type="hidden" name="_method" value="DELETE"
    span Signed in as {viewer}.
    a href="/account/edit" Your account
    button Sign out
if flash != ""
  p.flash = flash
if viewer != ""
  form.sign method="post" action="/entries" enctype="multipart/form-data"
    textarea name="message" placeholder="say something" required="required" = form.message
    if form.problems["message"] != none
      p.problem = form.problems["message"]
    label
      span A photo, if you like
      input type="file" name="photo" accept="image/*"
    button Sign
ol.entries
  for e in signatures
    li
      p.message = e.message
      if e.photo != ""
        img.photo src="/senro/blobs/{e.photo}" alt="a photo from {e.name}"
      p.who = e.name
      == removals["{e.id}"]
conventions

what senro decides

each convention is a kanso function the compiler runs during the build. what it writes is checked like code someone wrote, and kanso derived prints it.

01

routes

every route is one of a resource's seven actions, placed by its module and its name. singular resources such as /profile follow the same rule. an old URL is kept with legacy_route_aliases, which redirects to the resource's route.

02

the schema

there is no schema file. the migrations are values, folded into the schema while the app compiles, and each model's record is declared from its table's columns. when the server starts it compares the live tables with that schema.

03

models

a model's record, its reader, its checked writes and its policies come from its table and the functions in its module. a write the policy refuses has no other way into the table.

04

views

templates are kata files, compiled to kanso functions. the same functions run on the server and, compiled to wasm, in the browser, so a page can be rendered from data on either side and the two agree byte for byte.

05

sessions

a session is a signed cookie. sign-in stretches passwords with PBKDF2 and salts them with the operating system's random bytes, and a session older than its limit is refused.

06

postgres

senro speaks Postgres's wire protocol in a client written in kanso, over TLS when the server asks for it. there is no C binding and no second database to abstract over.

refusals

what senro refuses

a mistake Rails reports when a request reaches it is a compile error here. a misspelled action is the plainest case.

spec/errors/misspelled_action/posts.kso
pub fn index _
  "every post"

pub fn destory _
  "gone"
kanso check
error[derive]: `destory` is not an action: name it index, new, create, show, edit, update or destroy
  --> ./posts.kso:4:8
   4 | pub fn destory _
              ^

the same goes for two routes that could answer one request, a path_to that names no route, a model field with no column, and a template that reads a field its record lacks.

next

where to go from here

getting started builds the guestbook one file at a time. the guides cover each part of the framework in turn, and PRINCIPLES.md states the beliefs behind its design. senro is young: the guestbook and a pantry planner are the apps it has been built against, and its source is at github.com/kanso-lang/senro.