Re[10]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Курилка Россия http://kirya.narod.ru/
Дата: 26.06.09 13:33
Оценка:
Здравствуйте, dimgel, Вы писали:

[cut]

D>В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами. Но как я уже говорил, мне этот use case на данный момент не интересен. (Если он мне вдруг станет интересен, я в ноль cклонирую шаблоны Lift в отдельный независимый модуль. Обеспечу совместимость, так сказать.)


Ты забыл ещё возможность правки шаблонов и смены их без перекомпиляции кода.
Плюс ещё сейчас, насколько я понимаю, не поддерживаются "темы" (прописываемые в конфиге, или выбираемые пользователем). Внешние шаблоны для этого не обязательны, конечно, но на них это реализуется практически тривиально.
Re[11]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 26.06.09 13:44
Оценка:
Здравствуйте, Курилка, Вы писали:

D>>В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами.

К>Ты забыл ещё возможность правки шаблонов и смены их без перекомпиляции кода.

Да это в сущности то же самое. Если я из Eclipse не вылезаю, мне что внешний шаблон править, что внедрённый — один хрен. Так что фича эта тоже полезна исключительно дизайнерам с дримвивером.

К>Плюс ещё сейчас, насколько я понимаю, не поддерживаются "темы" (прописываемые в конфиге, или выбираемые пользователем). Внешние шаблоны для этого не обязательны, конечно, но на них это реализуется практически тривиально.


Под тривиальностью ты имеешь в виду — тупо копируем каталог с шаблонами и подкручиваем копию? Ну в общем да. Тут правда имеется небольшая засада — деплоймент. Либо мы пакуем все шаблоны всех тем в war-файл, и тогда каждая правка требует перепаковки (что отличается от перекомпиляции только затратами времени, да и то не сильно, если не всё подряд перекомпилять), либо мы держим шаблоны на сервере в отдельном месте, в распакованном виде, и огребаем геморрой с деплойментом. Всё это звучит неприятно, но не страшно; в конце концов, есть rsync, так что всё автоматизируемо.

// Начал было писать про то, что под существенные изменения дизайна практически всегда приходится перезатачивать логику. Но потом очухался: разные темы — это несколько попроще, чем разные дизайны.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: meowth  
Дата: 28.06.09 11:53
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Всем привет.


D>В общем, сабж. Будет опен-сорц, public domain. Там всё очень сыро, не утрясены даже некоторые ключевые архитектурные решения. Но парой примерчиков хочется поделиться. Один сегодня, один завтра-послезавтра. То, что утрясено. Пока что в виде статей, без ссылок на скачу исходников (вылью как утрясу кое-что, если появятся заинтересованные). Всё по-английски, т.к. планирую интервенцию в scala community.



А Lift вам чем не УГодил?
Re[2]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 28.06.09 12:25
Оценка:
Здравствуйте, meowth, Вы писали:

M>А Lift вам чем не УГодил?


1) Он stateful с дикими требованиями к памяти даже на простейших задачах. На моём VPS успешно крутится PHP-проектик под весьма приличной нагрузкой, но попытка перетащить пару активно используемых скриптов на Lift немедленно привела к переполнению памяти. С такими требованиями недолго и PHP полюбить. Ну а вконец добили меня uploaded files, которые он целиком загружает в память, хотя существует Apache FileUpload — стандарт де-факто (а в Servlet 3.0 — уже и де-юре). И это блин называется 1.0 release.

2) Про single responsibility principle автор лифта походу вообще не слышал. Его сверх-убогий, не годный ни на что кроме хомяков Васи Пупкина, ORM (что он и сам признаёт открытым текстом: если у вас больше пары десятков таблиц или имеются сложные joins, то юзайте JPA). Понатыкано в этом ORM всё в кучу — и persistence, и validation, и presentation. Я тут где-то успел порадовался на эту тему, но увы, радует это только первые пару дней. А уж на список ответственностей класса Menu из его книги, и на реализацию всего этого вообще смотреть нельзя без истерики.

Короче, перегруженная помойка дурных велосипедов.

3) Да и шаблоны внешние я не хочу. Все эти bind() со строковыми константами. Раздражает.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[3]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: dimgel Россия https://github.com/dimgel
Дата: 28.06.09 13:06
Оценка:
D>Про single responsibility principle автор лифта походу вообще не слышал.

И не только про него. С глобальными конфигурациями и thread-local переменными мужик, прямо скажем, чуток переборщил.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[4]: Я тут потихоньку ковыряю веб-фреймворк на scala...
От: Mamut Швеция http://dmitriid.com
Дата: 29.06.09 08:46
Оценка:
Здравствуйте, dimgel, Вы писали:

d> D>Про single responsibility principle автор лифта походу вообще не слышал.


d> И не только про него. С глобальными конфигурациями и thread-local переменными мужик, прямо скажем, чуток переборщил.


Ну, что поделать. Первопроходцам тяжело
avalon 1.0rc1 rev 239, zlib 1.2.3


dmitriid.comGitHubLinkedIn
Пример 1 (по-русски)
От: dimgel Россия https://github.com/dimgel
Дата: 29.06.09 09:47
Оценка:
package ru.dimgel.lib.web.example.ru.hello01

/*
 * _root_ указан, т.к. в противном случае компилятор ищет внутри пакета 
 * ru.dimgel.lib.web.example.ru вместо _root_.ru.
 */
import _root_.ru.dimgel.lib.web.core
import core.request.Request
import core.response.HTMLResponse

/*
 * DISCLAIMER: Всё это пока что на ранней стадии разработки. Примеры работают, 
 *             но вещи, упомянутые в комментариях, возможно ещё нет.
 * 
 * Это простейший пример "Hello world". Чтобы запустить его в виде отдельного 
 * веб-приложения (вне данного проекта), ваш web.xml должен выглядеть так:
 * 
 * <?xml version="1.0" encoding="utf-8"?>
 * <!DOCTYPE web-app PUBLIC 
 *     "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" 
 *     "http://java.sun.com/dtd/web-app_2_3.dtd">
 * <web-app>
 *     <servlet>
 *         <servlet-name>servlet</servlet-name>
 *         <servlet-class>ru.dimgel.lib.web.core.Servlet</servlet-class>
 *         <init-param>
 *             <param-name>main</param-name>
 *             <param-value>ru.dimgel.lib.web.example.ru.hello01.Main</param-value>
 *         </init-param>
 *        </servlet>
 *        <servlet-mapping>
 *         <servlet-name>servlet</servlet-name>
 *         <url-pattern>/ *</url-pattern>
 *     </servlet-mapping>
 * </web-app>
 * 
 * ЗАМЕЧАНИЕ: Между символами / и * внутри <url-pattern> пробела быть не должно. 
 *       Я добвил пробел, чтобы компилятор не ругался на вложенный комментарий.
 * 
 * Точкой входа в ваше веб-приложение является ваш подкласс класса core.Main. Метод 
 * service() выполняет обработку входящих запросов. В данном примере, мы генерируем 
 * простой HTMLResponse в ответ на любые запросы.
 * 
 * Lib.web интенсивно использует систему типов языка Scala. В сущности, одной из основных 
 * моих целей было получить максимальную поддержку от IDE, а также максимально ограничить 
 * пользователям библиотеки возможности по отстреливанию себе ног, через использование
 * статической типизации.
 * 
 * Одно из основных следствий данного подхода - я использую встроенную в Scala поддержку
 * XML вместо внешних шаблонизаторов. В будущих примерах я планирую показать, каким
 * образом различные варианты композиции и использования шаблонов могут быть выражены
 * в чисто функциональном стиле одними лишь средствами языка Scala. Также я планирую
 * добавить некоторые преобразования HTMLResponse (например, для "head merging"), но
 * это будут просто вспомогательные утилиты; система шаблонов в lib.web как таковая
 * отсутствует. Однако, вы можете использовать с lib.web любой шаблонизатор и отдавать
 * результат его работы на вход HTMLResponse (если результат - NodeSeq) или WriterResponse 
 * (если результат - String).
 * 
 * Подробнее мои аргументы за и против шаблонизаторов см. в обсуждении на форуме RSDN:
 * http://rsdn.ru/forum/web/3444298.aspx
 * 
 * В lib.web существуют расширяемые иерархии классов запросов и ответов. Цель, как уже было
 * сказано, - выразить ограничения протокола HTTP и Servlet API через систему типов Scala.
 * Например, доступ к InputStream запроса предоставляется только классом RawRequest,
 * а интерфейс к частям запроса "multipart/form-data" (т.е. к выгруженным файлам) - классом 
 * MultipartRequest. Метод service() может использовать pattern matching для диспетчеризации
 * запросов, как показано в следующем примере (example/ru/dispatch02).
 * 
 * Экземпляр класса-ответа (подклассов класса Response) создаётся кодом приложения
 * в процессе обработки запроса. Это функциональный подход - рассматривать метод service()
 * как функция (Request) => Response. Такой подход позволяет удобно использовать иерерхию
 * классов Response и обеспечивать различные API у разных подклассов Response. Это 
 * используется, в частности, при обработке ошибок (показано также в следующем примере).
 * 
 * Кстати говоря, я предпочитаю фабричные методы вместо оператора new. Во-первых, из-за
 * краткости, а во-вторых, реализацию фабричных методов проще поменять прозрачным для 
 * пользователя образом. Поэтому я запрещаю использование new всюду, где могу. Если вы 
 * попробуете написать "new HTMLResponse(...)", вы получите ошибку компиляции.
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  override def service(rq: Request) = HTMLResponse(rq,
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
      </head>
      <body>
        <h1>Превед!</h1>
        <p>Это простейший пример использования <strong>lib.web</strong>.</p>
      </body>
    </html>
  )
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 2 (по-русски)
От: dimgel Россия https://github.com/dimgel
Дата: 29.06.09 09:47
Оценка:
package ru.dimgel.lib.web.example.ru.dispatch02

import java.util.regex.Pattern
import scala.xml.NodeSeq
import _root_.ru.dimgel.lib.web.core
import core.request._
import core.response._

/*
 * Это второй пример lib.web. Он демонстрирует диспетчеризацию запросов, обработку
 * ошибок, и заодно surrounding template - эквивалент тега <lift:surround with="...">
 * средствами языка Scala).
 
 * Данный пример обрабатывает запросы для двух обычных страниц (/ и /about/me),
 * перенаправляет запросы статичного контента к сервлету "default" (показаны три 
 * варианта использования), выводит собственную страницу 404 для запросов вида 
 * /missing/handled/..., выводит дефолтную (генерируемую контейнером) страницу 404
 * для всех остальных запросов вида /missing/...
 *
 * ЗАМЕЧАНИЕ О ШАБЛОНАХ
 * 
 * В основе моего подхода к созданию шаблонов (в частности surrounding template в данном
 * примере) лежит взгяд на любой шаблон как на функцию (данные) => NodeSeq. Это вполне
 * универсальный функциональный подход, и я уверен, что любые варианты использования
 * шаблонов могут быть выражены в его рамках.
 * 
 * Единственная проблема - решить, какие данные подавать на вход шаблона. Поскольку 
 * шаблоны - это обычные функции, как и методы бизнес-логики, можно очень легко прийти 
 * к бардаку. Я считаю, что это проблема самодисциплины программиста - грамотное 
 * разделение логики от преставления. 
 * 
 * Я планирую сделать ещё один пример, посвящённый исключительно разным вариантам 
 * композиции и использования шаблонов, включая рассмотрение паттерна "view first",
 * на основе которого построена вся обработка запросов в Lift framework. А пока что,
 * в данном примере я подчеркнул проблему, добавив вложенные функции template()
 * внутрь методов home(), aboutMe() и error404().
 * 
 * На всякий случай повторю ссылку на мои соображения касательно внешних шаблонизаторов
 * на форуме RSDN: http://rsdn.ru/forum/web/3444298.aspx
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  
  /*
   * Метод willService(pathInfo) вызывается из core.Servlet при получении запроса в самую 
   * первую очередь, до какой-либо дальнейшей обработки (даже до создания экземпляра 
   * подкласса Request). Параметр pathInfo - это HttpServletRequest.pathInfo. Если данный 
   * метод возвращает false, запрос тут же перенаправляется сервлету "default" 
   * (контейнеру), без какой-либо дальнейшей обрабоки запроса.
   *
   * Реализация по умолчанию всегда возвращает true. Реализация, показанная в данном
   * примере, предполагает, что подкаталоги /images/, /js/ and /css/ содержат только
   * обычные файлы и должны обслуживаться сервлетом по умолчанию (контейнером).
   * 
   * В дополнение к данному методу, фреймворк всегда перенаправляет контейнеру запросы,
   * у которых pathInfo начинается с "/lib.web/". Эта проверка выполняется до вызова
   * метода willService(). (Я планирую поместить в этот подкаталог query.js и прочие
   * вещи того же рода.) 
   * 
   * NOTE: В данный момент я использую один общий проект для lib.web и примеров. Данный 
   *       пример замапен на /example/ru/dispatch02/ в файле web.xml, при этом файл 
   *       /lib.web/example.css отдаётся другим экземпляром core.Servlet, который замапен
   *       на корень /. Поэтому в данном примере я использую относительный путь 
   *       ../../../lib.web/example.css, и всё работает.
   */
  override def willService(pathInfo: String) =
    pathInfo == null || !Pattern.matches("^/(images|js|css)/.*$", pathInfo)
  
  /*
   * Метод service() обычно занимается не генерацией ответа, а анализом (с использованием
   * pattern matching) и диспетчеризацией запросов к соответствующим обработчикам.
   * У запроса может проверяться метод и путь (а также собственно класс запроса -
   * конкретный подкласс класса Request, но в данном примере это не показано).
   * 
   * Класс request.Method - это enum. Я кодирую enum следующим образом:
   * 
   * sealed abstract class Method(name: String) extends NotNull
   * object Method {
   *   object GET extends Method("GET")
   *   object POST extends Method("POST")
   *   ...
   * 
   *   // Фабричный метод:
   *   def apply(name: String): Method = ...
   * }
   * 
   * Класс request.Path - это представление HttpServletRequest.pathInfo, удобное для
   * pattern matching. Оно состоит из, и может быть сопоставлено со списком List[String]
   * компонент пути (разделённых слешем) и с булевым значением trailingSlash (хвостовой
   * слеш). Например:
   * 
   * > Path(List("about", "me"), false) соответствует pathInfo="/about/me",
   * > Path(List("about", "me"), true) соответствует pathInfo="/about/me/",
   * > Path(Nil, false) соответствует pathInfo=null.
   * 
   * Приведённая в данном примере реализация метода service() сопоставляет объект-запрос
   * целиком. Если вам, например, нужно сопостовлять только компоненты пути, это можно 
   * сделать короче:
   * 
   * rq.path.components match {
   *   case Nil => home(rq)
   *   case List("about", "me") => aboutMe(rq)
   *   ...
   * }
   */
  override def service(rq: Request) = rq match {
    case Request(Method.GET, Path(Nil, _)) => home(rq)
    case Request(Method.GET, Path(List("about", "me"), _)) => aboutMe(rq)

    /*
     * Для путей вида /missing или /missing/... мы возвращаем ErrorResponse; фреймворк 
     * передаст обработку этих запросов методу serviceError(). Конструктор (вернее, 
     * фабричный метод) ErrorResponse принимает дополнительный аргумент message, 
     * здесь опущенный. 
     */
    case Request(_, Path(List("missing", _*), _)) => ErrorResponse(rq, Status.NotFound)

    /*
     * Реализация метода core.Main.service(Request) перенаправляет запрос сервлету 
     * "default" (контейнеру), путём возврата ForwardResponse(rq). Этот ответ можно 
     * вернуть и самостоятельно, с тем же эффектом. Это полезно для отдачи статического 
     * контента.
     */
    case _ => super.service(rq)
    //case _ => ForwardResponse(rq)
  }
  
  /*
   * Метод serviceError(ErrorRequest) вызывается фреймворком, если метод service(Request)
   * вернул ErrorResponse.
   * 
   * Несколько дополнительных замечаний насчёт service(), serviceError(), ErrorRequest и
   * ErrorResponse (пожалуйста, сначала прочитайте код и комментарии метода serviceError()):
   * 
   * > Все подклассы Response принимают первым параметром ссылку на Request.
   *   Попытка создать более одного ответа для одного запроса приведёт к выбросу
   *   IllegalStateException.
   * 
   * > Попытка вернуть из методов service() или serviceError() ответ, созданный для 
   *   другого запроса (отличного от переданного в параметре метода), приведёт к 
   *   выбросу AssertionError.
   * 
   * > Объекты запросов и ответов не являются thread-safe. Вся обработка запроса должна
   *   выполняться в одном потоке. Теперь хорошие новости: классы core.Servlet и core.Main
   *   не содержат статических данных, поэтому вы можете объявлять в одном web.xml 
   *   произвольное количество приложений (как и сделано в данном проекте: на каждый 
   *   пример - отдельный <servlet> и <servlet-mapping>, и всё в одном web.xml).
   */
  override def serviceError(rq: ErrorRequest) = rq match {
    
    /*
     * Генерируем собственную страницу ошибки 404 для путей вида /missing/handled и
     * /missing/handled/... Класс ErrorRequest сопостовляется по полям method, path, 
     * status и message, причём status и message заполняются фреймворком из экземпляра 
     * ErrorResponse, возвращённого методом service().
     */
    case ErrorRequest(_, Path(List("missing", "handled", _*), _), Status.NotFound, _) => 
      error404(rq)
    
    /*
     * Реализация метода core.Main.serviceError() выводит страницу ошибки, сгенерированную 
     * контейнером. Такой же эффект можно получить, вернув объект ErrorResponse(rq).
     * 
     * Фабричный метод ErrorResponse(ErrorRequest) создаёт точно такой же экземпляр 
     * ErrorResponse, какой был возвращён методом service() - с такими же значениями 
     * полей status и message. Впрочем, вы можете вызвать полную форму - 
     * ErrorResponse(Request, status, message).
     * 
     * Возврат ErrorResponse из serviceError() в итоге приводит к вызову метода
     * HttpServletResponse.sendError(): sendError(status.code, message) если 
     * message != null, или sendError(status.code) в противном случае.
     */
    case _ => super.serviceError(rq)
    //case _ => ErrorResponse(rq)
  }
  
  /*
   * Обработчик главной страницы, вызывается из service().
   * Не содержит логики, только шаблон.
   */
  private def home(rq: Request) = {
    def template = tSurround(rq, 
      <h1>Главная</h1>
      <p>Это второй пример в дистрибутиве {tLibWeb}.</p>
      <p>Наводите мышью на пункты главного меню для получения подсказок, и кликайте на них чтобы проверить их работу.</p>
      <p>Все рассказы что и как - в комментариях в исходном коде примера.</p> 
    )
    HTMLResponse(rq, template)
  }
  
  /*
   * Обработчик страницы /about/me, вызывается из service().
   * Не содержит логики, только шаблон.
   */
  private def aboutMe(rq: Request) = {
    def template = tSurround(rq, 
      <h1>Обо мне</h1>
      <p>Йа примерко. См. мой исходный код и комментарии в нём.</p>
    )
    HTMLResponse(rq, template)
  }

  /*
   * Обработчик для страниц /missing/handled и /missing/handled/..., вызывается из 
   * serviceError(). Не содержит логики, только шаблон.
   *
   * Обратите внимание, что здесь конструктору HTMLResponse передаётся дополнительный
   * параметр status, со значением, взятым из обрабатываемого запроса ErrorRequest.
   * Это значение Status.NotFound, т.к. ErrorRequest создаётся фреймворком только
   * на основании ErrorResponse, возвращённого методом service(), а в методе service()
   * мы возвращаем только один ErrorResponse - со статусом Status.NotFound.
   */
  private def error404(rq: ErrorRequest) = {
    def template = tSurround(rq,
      <h1>Ошибка 404</h1>
      <p><strong>Страница не найдена: {rq.path}</strong></p>
      <p>Данная страница ошибки генерируется для путей вида <em>/missing/handled</em> и <em>/missing/handled/...</em>.</p>
    )
    HTMLResponse(rq, rq.status, template)
  }
  
  /*
   * Шаблона для логотипа lib.web.
   * В данном примере глобально используемые функции-шаблоны имеют имена вида "tXXX".
   */
  private def tLibWeb = <span class="libweb">lib.web</span>
  
  /*
   * Surrounding template.
   * В данном примере глобально используемые функции-шаблоны имеют имена вида "tXXX".
   * 
   * Обратите внимание, как объявлен параметр content: как "ленивое" значение, вычисляемое
   * в момент использования. Если вам нужно, чтобы значение content вычислялось до вызова 
   * tSurround(), объявите этот параметр обычным образом.
   *
   * Данный шаблон также принимает параметр Request, который нужен для генерации элемента
   * <base/>. Это к вопросу о том, "какие данные подавать на вход шаблона". Здесь я 
   * использовал простейшее решение. Я постараюсь пройтись подробнее по этой теме в будущих 
   * примерах.
   */
  private def tSurround(rq: Request, content: => NodeSeq) =
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={rq.baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../../lib.web/example.css"/>
      </head>
      <body>
        <div class="mainmenu">
          <a href="../../../lib.web/example.css" title="Перенаправляется фреймворком контейнеру, т.к. путь начинается с /lib.web/. 
                       Касательно использования относительных путей в HTML-коде, см. замечание в в комментариях к методу willService().">/lib.web/</a>
          <a href="js/script.js" title="Перенаправляется контейнеру, т.к. willService() возвращает false для подкаталога /js/">!willService()</a>
          <a href={rq.baseUrl} title="Генерируется методом home(), вызываемым из service()">service(): Главная</a>
          <a href="about/me" title="Генерируется методом aboutMe(), вызываемым из service()">service(): Обо мне</a>
          <a href="file.txt" title="Перенаправляется контейнеру методом service() путём вызова super.service()">super.service()</a>
          <a href="missing/handled" title="Генерируется методом error404(), вызываемым из serviceError()">serviceError()</a>
          <a href="missing/handled/again" title="Генерируется методом error404(), вызываемым из serviceError()">тоже serviceError()</a>
          <a href="missing/unhandled" title="Генерируется контейнером, путём вызова super.serviceError() из метода serviceError()">super.serviceError()</a>
        </div>
        {content}
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 3: параметры запроса - валидация, конвертация.
От: dimgel Россия https://github.com/dimgel
Дата: 11.07.09 06:11
Оценка:
package ru.dimgel.lib.web.example.ru.e03param

import java.util.regex.Pattern
import scala.xml.NodeSeq
import _root_.ru.dimgel.lib.web.core
import core.param._
import core.request._
import core.response._

/*
 * Данный пример демонстрирует доступ к параметрам запроса, валидацию значений параметров
 * и преобразование их к другим типам данных (конвертацию). В примере используются
 * встроенные в lib.web валидаторы и конвертеры значений. Каким образом можно создавать 
 * свои собственные валидаторы и конвертеры, будет продемонстрировано в следующем примере 
 * (e04customparam).
 * 
 * Я стараюсь излагать последовательно, но вероятно, данный пример нужно будет прочитать
 * дважды. Рекомендуется попробовать в работе все приведённые в данном примере TestCases, 
 * вводя в поля формы различные правильные и неправильные значения. Адрес работающего 
 * примера: 
 * 
 *     http://dimgel.ru:8080/lib.web/example/ru/e03param/
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  
  case class TestCase(title: String, result: Param.Value[_])
  
  override def service(rq: Request) = {
    /*
     * Свойство Request.param является экземпляром класса core.param.ParameterMap. 
     * С полным интерфейсом этого класса я ещё не определился (да и с именем тоже), 
     * но как минимум у него имеется метод apply(name: String), возвращающий экземпляр 
     * класса core.param.Param.
     * 
     * В данном примере мы будем использовать три параметра (nhb поля формы) - текстовую 
     * строку (text), целое десятичное число (int) и целое шестнадцатеричное число (hex):
     */
    val text = rq.param("text")
    val int = rq.param("int")
    val hex = rq.param("hex")
    
    /*
     * В общем случае параметры могут иметь по нескольку значений (например, multiselect
     * list). Поэтому внутреннее представление данных параметра - это List[String]. Вы
     * можете получить этот список, обратившись к свойству Param.data. Объявление класса
     * Param выглядит следующим образом:
     * 
     *     final case class Param(data: List[String]) extends NotNull { ... }
     * 
     * Большинство валидаторов и конвертеров (в частности, все, упомянутые в данном
     * примере) будут работать только с первым значением из списка (data.head) -
     * это наиболее часто используемый случай.
     * 
     * ЗАМЕЧАНИЕ. Этот же класс Param будет использован для доступа к заголовкам запроса,
     *            которые также могут иметь по нескольку значений (ещё не сделано).
     * 
     * Начнём. Чтобы определить, была ли засабмичена форма, проверяем наличие любого из 
     * параметров. Метод Param.isFound возвращает true если список значений параметра не 
     * пуст (data != null && data != Nil).
     */
    val submitted = rq.param("text").isFound
    
    /*
     * Этот список объектов TestCase мы передадим на вход шаблона.
     */
    val testResults =
      if (!submitted) Nil
      else {
        List (
          /*
           * Основной (канонический) способ получить значение параметра, приведённое к 
           * нужному типу (как правило это строка, число или дата), - это вызвать метод:
           *
           *     class Param {
           *       def apply[D] (validator, converter, defaultValue: D): Param.Value[D]
           *       ...
           *     }
           * 
           * Здесь validator - это цепочка валидаторов значения параметра; converter -
           * преобразователь значения из List[String] к нужному типу; defaultValue 
           * (значение по умолчанию) является обязательным - чтобы не забывали.
           * 
           * Типы данных валидаторов и конвертеров, входящих в lib.web, согласованы между 
           * собой и с типом значения по умолчанию. Например, вы не сможете записать в 
           * один вызов Param.apply() валидатор email-адреса (String), конвертер в целое 
           * число (Int) и значение по умолчанию типа Date. Эти ограничения форсируются
           * на этапе компиляции с помощью системы типов (generics + variance). Подробнее
           * о том, как именно это реализовано, см. в следующем примере (e04customparam).
           * 
           * Возвращаемый объект Param.Value объявлен следующим образом:
           *
           *     final case class Value[D](get: D, errors: List[VError]) extends NotNull {
           *       val ok = errors.isEmpty
           *     }
           * 
           * Он инкапсулирует значение параметра (либо значение по умолчанию) и список
           * ошибок валидации. Класс VError - это простой враппер над сообщением об ошибке
           * валидации:
           * 
           *     case class VError(message: String) extends NotNull
           * 
           * Приведённый ниже TestCase демонстрирует простейший и часто используемый 
           * случай - получение значения необязательного параметра в виде строки. Если 
           * параметр не задан или содержит пустое значение, будет возвращена пустая
           * строка:
           */
          TestCase(
            """text(VOptional, CString, "")""", 
            text(VOptional, CString, "")),
          
          /*
           * Имена валидаторов имеют вид Vxxx, имена конвертеров  - Cxxx. И те, и другие 
           * объявлены в пакете core.param.
           * 
           * Цепочки валидаторов всегда должны начинаться либо с VRequired, либо с 
           * VOptional. Это ограничение форсируется на этапе компиляции.
           * 
           * Оба валидатора - VRequired и VOptional - проверяют метод Param.isEmpty. 
           * Если параметр пуст (Param.data == null, Nil, или первое значение в списке 
           * является null или пустой строкой), то:
           * 
           * - VOptional-валидация считается успешной и возвращается значение по умолчанию 
           *   (без выполнения остальных валидаторов в цепочке);
           * 
           * - VRequired-валидация возвращает ошибку и значение по умолчанию 
           *   (также без выполнения остальных валидаторов в цепочке).
           * 
           * Если же параметр не пуст, то выполняются остальные валидаторы в цепочке.
           * При успешной валидации вызывается конвертер и возвращается сконвертированное
           * значение, в противном случае возвращается список ошибок и значение по 
           * умолчанию.
           * 
           * Такое решение позволяет упростить остальные валидаторы и конвертеры, избавив 
           * их от необходимости проверять значение параметра на непустоту. Кроме того,
           * получающийся в итоге вызов Param.apply() удобно и однозначно читается.
           */
          TestCase(
            """text(VRequired, CString, "hello")""", 
            text(VRequired, CString, "hello")),
          
          /*
           * Все стандартные валидаторы, кроме VOptional, принимают необязательный
           * параметр message - сообщение об ошибке валидации. По умолчанию выводится
           * сообщение на английском, индивидуальное для каждого валидатора.
           */
          TestCase(
            """text(VRequired(msg), CString, "goodbye")""", 
            text(VRequired("Введите строку"), CString, "goodbye")),
          
          /*
           * Как уже было сказано, валидаторы могут группироваться в цепочки.
           * Для этого используются операторы: "|" после VOptional, "&" после VRequired,
           * "&" и "&&" (short-circuit and full-circuit "and") после остальных валидаторов.
           */
          TestCase(
            """text(VOptional | VEmail, CString, "default")""", 
            text(VOptional | VEmail, CString, "default")),
          TestCase(
            """text(VRequired & VEmail(msg), CString, "default")""", 
            text(VRequired & VEmail("На Email не похоже"), CString, "default")),
          
          /*
           * Закомментированная ниже строка не скомпилируется, т.к. цепочка
           * валидаторов начинается не с VRequired или VOptional.
           */
          //TestCase("wrong", text(VEmail, CString, "")),
          
          /*
           * Ниже следуют TestCases с более длинными цепочками, использующими short-circuit 
           * и full-circuit "and". Валидаторы выполняются слева направо. Оператор "&" 
           * прекращает валидацию, если его левый параметр (часть цепочки слева от него)
           * вернул ошибку.
           * 
           * Операторы также можно группировать с помошью скобок. При этом реализация 
           * операторов VRequired.& и VOptional.| такова, что они неявно ставят скобки 
           * вокруг всех остальных валидаторо в цепи:
           * 
           *     VRequired & V1 & V2  ==>  VRequired & (V1 & V2)
           *     VOptional | V1 & V2  ==>  VOptional | (V1 & V2)
           *     (VRequired & V1 & V2) & V3  ==>  VRequired & ((V1 & V2) & V3)
           *     (VOptional | V1 & V2) & V3  ==>  VOptional | ((V1 & V2) & V3)
           * 
           * То есть, в корне дерева валидаторов всегда будут операции VRequired.& или
           * VOptional.|.
           * 
           * Валидатор в нижеприведённом TestCase читается так: обязательный параметр, 
           * длина 5-10 символов, должен быть числом (возможно, с лидирующими и хвостовыми
           * пробелами). Здесь также фигурирует разрешающий пробелы конвертер в целое 
           * (Int), и значение по умолчанию Int.
           */
          TestCase(
            """int(VOptional | VLen(5, 10) & VTrimInt, CTrimInt, 0)""", 
            int(VOptional | VLen(5, 10) & VTrimInt, CTrimInt, 0)),
        
          /*
           * Отличия данного TestCase от предыдущего: пробелы не разрешены, используется 
           * full-circuit "and". То есть, если вы введёте в поле int, например, строку 
           * "ааа", предыдущий валидатор выдаст только ошибку VLen (неправильная длина 
           * строки) и на этом остановится (т.к. short-circuit), а данный валидатор выдаст 
           * обе ошибки: VLen и VInt.
           */
          TestCase(
            """int(VOptional | VLen(5, 10) && VInt, CInt, 0)""", 
            int(VOptional | VLen(5, 10) && VInt, CInt, 0)),
          
          /*
           * Как уже упоминалось, стандартные валидаторы и конвертеры согласованы 
           * по типам данных между собой и со значениями по умолчанию. Как именно это
           * реализовано, подробно рассказано в следующем примере (e04customparam),
           * демонстрирующем создание собственных валидаторов и конвертеров. 
           * 
           * Нижеследующие закомментированные TestCases не скомпилируются:
           * 1) в первом указан конвертер целого без валидатора целого;
           * 2) во втором указано строковое значение по умолчанию с валидатором и 
           *    конвертером целого;
           * 3) в третьем используется валидатор целого, допускающий пробелы, с 
           *    конвертером целого, не допускающим пробелы (а вот наоборот можно).
           */
          //TestCase("wrong", int(VOptional, CInt, 0)),
          //TestCase("wrong", int(VOptional | VInt, CInt, "")),
          //TestCase("wrong", int(VOptional | VTrimInt, CInt, 0)),
          
          /*
           * На данный момент набор конкретных валидаторов и конвертеров - сырой и
           * ограниченный. Нет работы с датами (необходимо для замены getDateHeader()).
           * Кроме того, я не до конца уверен, не перемудрил ли я с Trim-валидаторами -
           * вроде бы всё хорошо, потому что строго, но выглядит несколько избыточно.
           * 
           * Что и как на данный момент реализовано, лучше всего смотреть в исходных 
           * текстах (core/param/validators.scala и core/param/converters.scala), а здесь 
           * я покажу ещё только один валидатор, весьма полезный сам по себе и интенсивно
           * используемый как базовый класс для многих других валидаторов: VRegex,
           * проверяющий первое значение параметра (Param.data.head) по регулярному 
           * выражению. Ниже показаны четыре формы фабричного метода для валидатор VRegex.
           */
          TestCase(
            """hex(VOptional | VRegex(Regex), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$".r), CString, "")),
          TestCase(
            """hex(VOptional | VRegex(Regex, msg), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$".r, "Не 16-ричное число"), CString, "")),
          TestCase(
            """hex(VOptional | VRegex(String), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$"), CString, "")),
          TestCase(
            """hex(VOptional | VRegex(String, msg), CString, "")""", 
            hex(VOptional | VRegex("^[0-9a-fA-F]+$", "Не 16-ричное число"), CString, ""))
        )
      }
    
    HTMLResponse(rq, template(rq, testResults))
  }
  
  /*
   * Шаблон страницы. Форма для ввода параметров свёрстана вручную. (Модуль форм будет 
   * показан в последующих примерах... когда я его добью.)
   * 
   * Обратите внимание, каким образом значения параметров копируются в поля ввода 
   * (в атрибуты <input value=.../>). Продемонстрированы три способа:
   * 
   * 1. В поле name="text" используется способ, показанный в самом первом TestCase:
   *    (VOptional, CString, "")
   * 
   * 2. В поле name="int" вместо трёх параметров (валидатор, конвертер и значение по
   *    умолчанию) передаётся один объект - PString. Этот объект наследуется из класса
   *    Param.Strategy, инкапсулирующего все три параметра (валидатор, конвертер, 
   *    значение по умолчанию) в одном объекте. Объявления таковы:
   * 
   *        class Param { 
   *          def apply[D] (Param.Strategy[D]): Param.Value[D]
   *          ...
   *        } 
   *        object Param { 
   *          abstract case class Strategy[D] (validator, converter, default: D)
   *          ...
   *        }
   *        object PString extends Strategy(VOptional, CString, "")
   *    
   *    То есть, это паттерн "стратегия". В данном случае использование PString
   *    полностью эквивалентно предыдущей "полной" записи.
   * 
   *    Я не вижу большой пользы от этой идеи в плане добавления стандартных стратегий в 
   *    библиотеку, т.к. слишком много ограничений: нужны отдельные стратегии для
   *    VRequired и VOptional, плюс непонятно как быть с сообщениями об ошибках. Но
   *    возможно, при разработке конкретных веб-приложений эта возможность окажется 
   *    востребованной.
   * 
   * 3. В поле name="hex" используется специальный метод Param.s, дающий результат,
   *    полностью эквивалентный двум предыдущим вариантам.
   */
  private def template(rq: Request, testResults: List[TestCase]) = 
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={rq.baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../../lib.web/example.css"/>
      </head>
      <body>
        <h1>Чтение параметров запроса</h1>
        <p>Это третий пример в <span class="libweb">lib.web</span>, демонстрирующий 
          валидацию и конвертацию параметров запроса.</p>
        <h2>Форма</h2>
        <form><table>
          <tr>
            <th>Text:</th>
            <td><input type="text" name="text" value={rq.param("text")(VOptional, CString, "").get}/></td>
          </tr>
          <tr>
            <th>Int:</th>
            <td><input type="text" name="int" value={rq.param("int")(PString).get}/></td>
          </tr>
          <tr>
            <th>Hex:</th>
            <td><input type="text" name="hex" value={rq.param("hex").s}/></td>
          </tr>
          <tr>
            <th/>
            <td><input type="submit" value="Submit"/></td>
          </tr>
        </table></form>
        <h2>Результаты</h2>
        {if (testResults.isEmpty)
          <p>Введите данные форму.</p>
        else
          <dl class="messages">{
            for (r <- testResults) 
            yield {
              val attr = if (r.result.ok) "info" else "error"
              <dt class={attr}>{r.title} = {r.result.get}</dt> ++ {
                for (VError(message) <- r.result.errors) 
                yield <dd class={attr}>{message}</dd> 
              }
            }
          }</dl>
        }
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Пример 4: создание своих валидаторов и конвертеров
От: dimgel Россия https://github.com/dimgel
Дата: 11.07.09 06:32
Оценка:
package ru.dimgel.lib.web.example.ru.e04customparam

import java.util.regex.Pattern
import scala.xml.NodeSeq
import scala.collection.mutable.ListBuffer
import _root_.ru.dimgel.lib.web.core
import core.param._
import core.request._
import core.response._

/*
 * Данный пример демонстрирует создание своих собственных валидаторов и конвертеров для 
 * параметров запроса. НЕОБХОДИМО ЗНАКОМСТВО С ПРЕДЫДУЩИМ ПРИМЕРОМ, демонстрирующим
 * использование параметров.
 * 
 * Здесь мы рассмотрим валидацию поля "возраст" - целого числа в диапазоне от 16 до 150
 * (лет; отличный диапазон для сайтов знакомств).
 * 
 * Материал сложный. Возможно, его придётся прочитать минимум дважды. Рекомендуется 
 * попробовать пример в работе по адресу:
 * 
 *     http://dimgel.ru:8080/lib.web/example/ru/e04customparam/
 */
class Main(servlet: core.Servlet) extends core.Main(servlet) {
  /*
   * Согласование использования валидаторов и конвертеров на этапе компиляции необходимо 
   * для того, чтобы не допустить ошибочного использования, например, конвертера целого 
   * числа без предварительного вызова валидатора целого числа. 
   * 
   * Это реализовано с помощью type parametrization (generics), в т.ч. с использованием
   * lower bounds. Все валидаторы и конвертеры помечаются generic-параметром - тегом 
   * (подклассом core.param.Tags.TAny). Иерархия тегов отражает совместимость валидаторов 
   * и конвертеров. Сигнатура метода Param.apply() задаёт требуемое отношение между тегами 
   * валидаторов и конвертеров.
   * 
   * 
   * НАПОМИНАНИЕ. Собственное представление данных в классе Param - это List[String] 
   * (параметры и заголовки запроса могут иметь несколько значений). Однако большинство 
   * валидаторов и конвертеров работают только с первым элементом списка значений, т.е. 
   * рассматривают параметр как имеющий единственное значение (наиболее частый вариант
   * использования параметров в реальной жизни). Наличие первого элемента гарантируется
   * специальными валидаторами VRequired и VOptional, один из которых обязан быть
   * первым в цепочке валидаторов (это проверяется на этапе компиляции). Так что все 
   * остальные валидаторы (и конвертеры) могут без дополнительных проверок обращаться 
   * к Param.data.head (гарантируется != null).
   * 
   * 
   * Рассмотрим для примера пару имеющихся в lib.web тегов, пару валидаторов и конвертер:
   * 
   *     package ru.dimgel.lib.web.core.param
   * 
   *     object Tags {
   *       trait TAny  // список значений
   *       trait TString extends TAny  // одно значение
   *       ...
   *       trait TTrimInt extends TString
   *       trait TInt extends TTrimInt
   *       ...
   *     }
   * 
   *     class VInt extends VImp[TInt]
   *     class VTrimInt extends VImp[TTrimInt]
   * 
   *     // Первый параметр Int - это тип возвращаемого конвертером значения.
   *     class CTrimInt extends Converter[Int, CTrimInt]
   * 
   * Валидаторы, помеченные тегом TTrimInt или его подклассом(!), пропускают параметр, 
   * первое значение которого является целым числом с необязательными лидирующими и 
   * хвостовыми пробелами (это отражено в инфиксе "Trim" имени тега). Аналогично, 
   * конвертеры, помеченные тегом TTrimInt или его подклассом(!), гарантируют успешную 
   * конвертацию (не важно во что) параметра, первое значение которого является целым 
   * числом с необязательными лидирующими и хвостовыми пробелами.
   * 
   * Например, тег TInt является подклассом TTrimInt, что означает буквально следующее: 
   * "если параметр проходит валидацию как целое число с пробелами, то он и подавно 
   * пройдёт валидацию как целое число без пробелов". Соответственно, валидатор VInt 
   * может быть использован совместно с конвертером CTrimInt.
   * 
   * Замечания по использованию множественного наследования тегов - ниже, в примере 
   * тега TAge.
   * 
   * 
   * Точная сигнатура метода Param.apply() выглядит следующим образом:
   * 
   *     final def apply[D, CT <: TAny, VT <: CT] ( 
   *       validator: VChain[VT], 
   *       converter: Converter[D, CT], 
   *       default: D
   *     ): Param.Value[D]
   * 
   * Здесь D - тип данных, возвращаемых конвертером, CT - тег конвертера, VT - тег
   * валидатора, TAny - базовый trait для тегов. (ЗАМЕЧАНИЕ. Параметр validator имеет тип 
   * VChain, а не VImp; это связано с реализацией требования, чтобы цепочки валидаторов
   * начинались с VRequired или VOptional.)
   * 
   * Видно, что взаимное соответствие валидаторов и конвертеров задаётся условием 
   * VT <: CT (VT - подкласс CT). Это гарантирует, что если валидатор успешно выполнил 
   * валидацию параметра, то конвертер успешно этот параметр сконвертирует.
   * 
   * 
   * Объявление тега TTrimAge следует читать аналогично: "возраст - частный случай целого;
   * если параметр проходит валидацию как TTrimAge, то он пройдёт валидацию и как TTrimInt".
   */
  trait TTrimAge extends Tags.TTrimInt
  
  /*
   * Валидатор VTrimAge наследуется из VImp (базового класса для всех валидаторов) и 
   * переопределяет виртуальный метод validate(Param).
   * 
   * Конструкторы валидатора объявлены приватными. Для инстанциирования валидатора нужно 
   * использовать объект-компаньон. Этот подход используется в lib.web повсеместно.
   */
  class VTrimAge private(message: String) extends VImp[TTrimAge] {
    private def this() = this("Возраст задан неверно.")
    
    def validate(p: Param): List[VError] = {
      p.data.head match {
        // Здесь trim не используется (вызов s.toInt вместо s.trim.toInt), т.к. s содержит 
        // группу в круглых скобках (\d{2}), а не всю исходную строку.
        case VTrimAge.regex(s) if { val i = s.toInt; i >= 16 && i <= 150 } => Nil
        case _ => VError(message) :: Nil
      }
    }
  }
  
  /*
   * Объект-компаньон наследуется из класса, что позволяет использовать его в цепочках
   * валидаторов (без лишней пустой пары скобок, как было бы в случае использования метода 
   * apply()). То есть, в итоге мы имеем ровно два разрешённых варианта записи валидатора 
   * в выражениях: VTrimAge и VTrimAge(message).
   */
  object VTrimAge extends VTrimAge {
    def apply(message: String) = new VTrimAge(message)
    
    // Используется в validate().
    private val regex = """^\s*(\d{2,3})\s*$""".r
  }
  
  /*
   * Это второй пример тега. Он наследуется сразу от двух тегов и означает следующее: "если 
   * параметр проходит валидацию как возраст без пробелов, то он пройдёт валидацию и как 
   * целое без без пробелов, и как возраст с пробелами".
   * 
   * При объявлении тегов нужно максимально аккуратно и полно описывать их взаимосвязи.
   * Здесь не надо пренебрегать множественным наследованием, т.к. это даёт дополнительную
   * гибкость. Например, в данном случае с валидатором, помеченным тегом TAge, могут 
   * быть использованы конвертеры, помеченные тегами TAge, TTrimAge, TTrimInt или TInt
   * (TInt и TTrimAge наследуются из TTrimInt).
   */
  trait TAge extends Tags.TInt with TTrimAge
  
  /*
   * Данный валидатор использует вспомогательный класс VFunc, наследуемый из VImp и 
   * принимающий параметром конструктора либо функцию (Param) => List[VError] (точно 
   * соответствующую прототипу виртуального метода validate()), либо два параметра: 
   * функцию (Param) => Boolean и message: String. 
   * 
   * В данном примере я использую второй вариант конструктора. Также здесь я не стал
   * делать класс с параметром message, а оставил только объект с жёстко прошитым
   * сообщением об ошибке. Так что вариант использования данного валидатора в выражениях
   * только один: VAge.
   * 
   * ЗАМЕЧАНИЕ. Для валидаторов, чью логику можно выразить через единственное регулярное
   * выражение, рекомендуется использовать базовый класс VRegex. Если использовать VRegex
   * в цепочках как самостоятельный валидатор, он имеет тег TString, а при наследовании 
   * из него можно указывать любой тег.
   */
  object VAge extends VFunc[TAge] (
    (p: Param) => {
      val s = p.data.head
      // TODO Как сделать этот вызов попроще, не ссылаясь на pattern, но без match?
      if (!"^\\d{2,3}$".r.pattern.matcher(s).matches) false
      else {
        val i = s.toInt
        i >= 16 && i <= 150
      }
    },
    "Возраст задан неверно."
  )
  
  /*
   * Реализация данного конвертера похожа на CTrimInt. Он возвращает целое число.
   * Разница только в теге и отсутствии вызова trim перед toInt.
   * 
   * Конвертеры не проверяют параметр на непустоту, т.к. эта проверка выполняется методом 
   * Param.apply() после валидации. Если валидация была успешной и параметр пуст (такое 
   * возможно только при использовании VOptional), то конвертер не вызывается, а 
   * Param.apply() возвращает значение по умолчанию.
   * 
   * ЗАМЕЧАНИЕ. Это неудобно в случае checkbox: unchecked checkbox сабмитит пустое 
   * значение Param(Nil). Нужно помнить, что значение по умолчанию всегда должно быть 
   * false. Поэтому в модуле форм под checkbox создан готовый класс с настроенными 
   * валидаторами и конвертерами, причём конвертер возвращает true при любом значении
   * параметра, а значение по умолчанию false.
   */
  object CAge extends Converter[Int, TAge] {
    def apply(p: Param) = Some(p.data.head.toInt)
  }
  
  /*
   * А теперь посмотрим, как всё это будет работать. Проверять будем на одной странице,
   * с вручную свёрстанной GET-формой, содержащей одно поле "age".
   */
  case class TestCase(title: String, result: Param.Value[_])
  
  override def service(rq: Request) = {
    val p = rq.param("age")
    val testResults =
      if (!p.isFound) Nil
      else
        List(
          /*
           * Закомментированная ниже строка не скомпилируется, т.к. VTrimAge помечен 
           * тегом TTrimAge, CAge - тегом TAge, и TTrimAge не является подклассом TAge:
           */
          //TestCase("VRequired & VTrimAge, CAge", p(VRequired & VTrimAge, CAge, 0)),
          TestCase("VRequired & VTrimAge, CTrimInt", p(VRequired & VTrimAge, CTrimInt, 0)),
          TestCase("VRequired & VAge, CTrimInt", p(VRequired & VAge, CTrimInt, 0)),
          TestCase("VRequired & VAge, CAge", p(VRequired & VAge, CAge, 0)),
          
          /*
           * В цепочках валидаторов тег каждого следующего валидатора должен быть
           * равен тегу предыдущего валидатора, или быть его подклассом. Причины тому
           * две:
           * 
           * 1) Лень возиться с ковариантностью: боюсь, мозги задымятся.
           * 
           * 2) Оно и так смотрится вполне логично: сначала проверяются более слабые 
           *    условия, затем более жёсткие. Кроме того, при таком подходе невозможно 
           *    включить в одну цепочку валидаторы для несовместимых тегов (например,
           *    TInt и TDate, не являющихся подклассами друг дружки).
           *    
           * Тег всей цепочки валидаторов равен тегу последнего валидатора в ней, т.е.
           * самому дальнему подклассу TAny.
           * 
           * Закомментированная ниже строка не скомпилируется, т.к. тег VAge помечен 
           * тегом TAge, тег VLen - тегом TString, при этом TAge является подклассом 
           * TString (TAge <: TInt <: TString). Чтобы использовать оба эти валидатора, 
           * нужно поменять их местами, как в раскомментированном примере ниже, где мы
           * имеем следующие отношения между тегами:
           * 
           *     Объекты: VRequired & VLen(2) &  VAge     Вся цепь    CInt
           *     Теги:    TAny   <:   TString <: TAge ==> TAge     >: TInt
           */
          //TestCase("VRequired & VAge & VLen(2), CAge", p(VRequired & VAge & VLen(2), CInt, 0)),
          TestCase("VRequired & VLen(2) & VAge, CAge", p(VRequired & VLen(2) & VAge, CInt, 0))
        )
    
    HTMLResponse(rq, template(rq.baseUrl, p(VOptional, CString, "").get, testResults))
  }
  
  private def template(baseUrl: String, age: String, testResults: List[TestCase]) = 
    <html>
      <head>
        <title>{getClass.getCanonicalName}</title>
        <base href={baseUrl}/>
        <link rel="stylesheet" type="text/css" href="../../../lib.web/example.css"/>
      </head>
      <body>
        <h1>Валидаторы возраста</h1>
        <p>Это четвёртый пример в <span class="libweb">lib.web</span>, демонстрирующий 
          создание собственных валидаторов и конвертеров.</p>
        <h2>Форма</h2>
        <form>
          <p>Ваш возраст (16-150): 
            <input type="text" name="age" value={age} style="width: 5em"/>
            <input type="submit" value="Проверить"/></p>
        </form>
        <h2>Примеры</h2>
        <ul>
          <li><a href="?age=16">"16"</a> &mdash; всё ок.</li>
          <li><a href="?age=%2016%20">" 16 " (с пробелами)</a> &mdash; срабатывают VAge и VLen; первый пример ok.</li>
          <li><a href="?age=150">"150"</a> &mdash; срабатывает VLen(2); остальные примеры ok.<br/></li>
          <li><a href="?age=">"" (пустой ввод)</a> &mdash; срабатывают VRequired.</li>
          <li><a href="?age=a">"a"</a> &mdash; срабатывают VTrimAge, VAge и VLen(2).</li>
          <li><a href="?age=aa">"aa"</a> &mdash; срабатывают VTrimAge и VAge.</li>
          <li><a href="?age=1">"1"</a> &mdash; срабатывают VTrimAge, VAge и VLen(2).</li>
          <li><a href="?age=11">"11"</a> &mdash; срабатывают VTrimAge и VAge.</li>
        </ul>
        <h2>Результаты</h2>
        {if (testResults.isEmpty)
          <p>Введите данные форму или кликните на ссылке в примерах.</p>
        else
          <dl class="messages">{
            for (r <- testResults) 
            yield {
              val attr = if (r.result.ok) "info" else "error"
              <dt class={attr}>{r.title}</dt> ++ {
                for (VError(message) <- r.result.errors) 
                yield <dd class={attr}>{message}</dd> 
              }
            }
          }</dl>
        }
      </body>
    </html>
}
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Версия 0.1.0 доступна для скачки.
От: dimgel Россия https://github.com/dimgel
Дата: 12.07.09 07:05
Оценка: 19 (1)
Всем привет.

Сабж: http://dimgel.ru/lib.web/files/

Начинать с README, разделы "как изучать" и "как запускать примеры".
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re: Версия 0.1.0 доступна для скачки.
От: Курилка Россия http://kirya.narod.ru/
Дата: 12.07.09 07:08
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Всем привет.


D>Сабж: http://dimgel.ru/lib.web/files/


D>Начинать с README, разделы "как изучать" и "как запускать примеры".


Можел лучше репозиторий было организовать?
Скажем, на гуглокоде, где бесплатный issue tracker есть?
Re[2]: Версия 0.1.0 доступна для скачки.
От: dimgel Россия https://github.com/dimgel
Дата: 12.07.09 07:17
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Можел лучше репозиторий было организовать?

К>Скажем, на гуглокоде, где бесплатный issue tracker есть?

Рано. Там слишком много грязи ещё, и я не хочу, чтобы мне мешали ставить дикие эксперименты, заглядывая в промежуточные коммиты.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[3]: Версия 0.1.0 доступна для скачки.
От: Курилка Россия http://kirya.narod.ru/
Дата: 12.07.09 07:20
Оценка: 5 (1) +1
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Можел лучше репозиторий было организовать?

К>>Скажем, на гуглокоде, где бесплатный issue tracker есть?

D>Рано. Там слишком много грязи ещё, и я не хочу, чтобы мне мешали ставить дикие эксперименты, заглядывая в промежуточные коммиты.


Дак не обязательно же всё коммитить в транк
А так feedback хоть какой-то был бы, думаю.
Но, хозяин — барин, конечно.
Re[4]: Версия 0.1.0 доступна для скачки.
От: dimgel Россия https://github.com/dimgel
Дата: 13.07.09 13:04
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Дак не обязательно же всё коммитить в транк


А можешь посоветовать, как лучше организовать? У меня нет опыта branche merging. Я пока подумываю что-то вроде следующего:
1) в рабочей ветке либа и примеры в одном проекте (до поры до времени, по крайней мере: так удобнее отлаживаться с `mvn jetty:run`);
2) созревшая версия копируется в отстойник, где разбивается на два проекта — либа отдельно, примеры отдельно (попутно удаляется всякий хлам);
3) вот это вылизанное копируется в так называемый "транк", который в данном случае транком собственно и не является.

Или как вариант (если google code такое позволяет) — даю доступ к tags (e.g. tags/0.1.0), в качестве отстойника использую branches (e.g. branches/0.1.0), а шуршу в закрытом trunk (так более по-человечески).
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re[5]: Версия 0.1.0 доступна для скачки.
От: Курилка Россия http://kirya.narod.ru/
Дата: 13.07.09 18:56
Оценка:
Здравствуйте, dimgel, Вы писали:

D>Здравствуйте, Курилка, Вы писали:


К>>Дак не обязательно же всё коммитить в транк


D>А можешь посоветовать, как лучше организовать? У меня нет опыта branche merging. Я пока подумываю что-то вроде следующего:

D>1) в рабочей ветке либа и примеры в одном проекте (до поры до времени, по крайней мере: так удобнее отлаживаться с `mvn jetty:run`);
D>2) созревшая версия копируется в отстойник, где разбивается на два проекта — либа отдельно, примеры отдельно (попутно удаляется всякий хлам);
D>3) вот это вылизанное копируется в так называемый "транк", который в данном случае транком собственно и не является.

D>Или как вариант (если google code такое позволяет) — даю доступ к tags (e.g. tags/0.1.0), в качестве отстойника использую branches (e.g. branches/0.1.0), а шуршу в закрытом trunk (так более по-человечески).


В "средствах разработки" спроси, не велик опыт бранчевания у меня .
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.