[cut]
D>В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами. Но как я уже говорил, мне этот use case на данный момент не интересен. (Если он мне вдруг станет интересен, я в ноль cклонирую шаблоны Lift в отдельный независимый модуль. Обеспечу совместимость, так сказать.)
Ты забыл ещё возможность правки шаблонов и смены их без перекомпиляции кода.
Плюс ещё сейчас, насколько я понимаю, не поддерживаются "темы" (прописываемые в конфиге, или выбираемые пользователем). Внешние шаблоны для этого не обязательны, конечно, но на них это реализуется практически тривиально.
Re[11]: Я тут потихоньку ковыряю веб-фреймворк на scala...
Здравствуйте, Курилка, Вы писали:
D>>В общем, единственный аргумент за внешний шаблонизатор, который я готов принять, — это дизайнеры с дримвиверами. К>Ты забыл ещё возможность правки шаблонов и смены их без перекомпиляции кода.
Да это в сущности то же самое. Если я из Eclipse не вылезаю, мне что внешний шаблон править, что внедрённый — один хрен. Так что фича эта тоже полезна исключительно дизайнерам с дримвивером.
К>Плюс ещё сейчас, насколько я понимаю, не поддерживаются "темы" (прописываемые в конфиге, или выбираемые пользователем). Внешние шаблоны для этого не обязательны, конечно, но на них это реализуется практически тривиально.
Под тривиальностью ты имеешь в виду — тупо копируем каталог с шаблонами и подкручиваем копию? Ну в общем да. Тут правда имеется небольшая засада — деплоймент. Либо мы пакуем все шаблоны всех тем в war-файл, и тогда каждая правка требует перепаковки (что отличается от перекомпиляции только затратами времени, да и то не сильно, если не всё подряд перекомпилять), либо мы держим шаблоны на сервере в отдельном месте, в распакованном виде, и огребаем геморрой с деплойментом. Всё это звучит неприятно, но не страшно; в конце концов, есть rsync, так что всё автоматизируемо.
// Начал было писать про то, что под существенные изменения дизайна практически всегда приходится перезатачивать логику. Но потом очухался: разные темы — это несколько попроще, чем разные дизайны.
... << RSDN@Home 1.1.4 stable SR1 rev. 568>>
Re: Я тут потихоньку ковыряю веб-фреймворк на scala...
Здравствуйте, dimgel, Вы писали:
D>Всем привет.
D>В общем, сабж. Будет опен-сорц, public domain. Там всё очень сыро, не утрясены даже некоторые ключевые архитектурные решения. Но парой примерчиков хочется поделиться. Один сегодня, один завтра-послезавтра. То, что утрясено. Пока что в виде статей, без ссылок на скачу исходников (вылью как утрясу кое-что, если появятся заинтересованные). Всё по-английски, т.к. планирую интервенцию в scala community.
Здравствуйте, 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, Вы писали:
d> D>Про single responsibility principle автор лифта походу вообще не слышал.
d> И не только про него. С глобальными конфигурациями и thread-local переменными мужик, прямо скажем, чуток переборщил.
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>
)
}
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: параметры запроса - валидация, конвертация.
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: создание своих валидаторов и конвертеров
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> — всё ок.</li>
<li><a href="?age=%2016%20">" 16 " (с пробелами)</a> — срабатывают VAge и VLen; первый пример ok.</li>
<li><a href="?age=150">"150"</a> — срабатывает VLen(2); остальные примеры ok.<br/></li>
<li><a href="?age=">"" (пустой ввод)</a> — срабатывают VRequired.</li>
<li><a href="?age=a">"a"</a> — срабатывают VTrimAge, VAge и VLen(2).</li>
<li><a href="?age=aa">"aa"</a> — срабатывают VTrimAge и VAge.</li>
<li><a href="?age=1">"1"</a> — срабатывают VTrimAge, VAge и VLen(2).</li>
<li><a href="?age=11">"11"</a> — срабатывают 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>
}
Здравствуйте, dimgel, Вы писали:
D>Здравствуйте, Курилка, Вы писали:
К>>Можел лучше репозиторий было организовать? К>>Скажем, на гуглокоде, где бесплатный issue tracker есть?
D>Рано. Там слишком много грязи ещё, и я не хочу, чтобы мне мешали ставить дикие эксперименты, заглядывая в промежуточные коммиты.
Дак не обязательно же всё коммитить в транк
А так feedback хоть какой-то был бы, думаю.
Но, хозяин — барин, конечно.
Здравствуйте, Курилка, Вы писали:
К>Дак не обязательно же всё коммитить в транк
А можешь посоветовать, как лучше организовать? У меня нет опыта 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 (так более по-человечески).
Здравствуйте, 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 (так более по-человечески).
В "средствах разработки" спроси, не велик опыт бранчевания у меня .