24 мая 2011

Common Lisp. Первый блин.

На базе библиотеки cl-wkb сделал cl-ewkb.

Итак, для чего же нужна данная библиотека? Начну сдалека, придумали как-то люди хранить геометрические объеты в стоблцах реляционных баз данных. Для ввода данных придумали два формата текстовый и бинарный, для человека и компьютера соответственно. Эти форматы используются для вставки, запроса данных в СУБД. Сама же СУБД как хочет так и хранить геометрические данные. Определили набор геометиреческих примитивов:
  • Точка
  • Линия
  • Выпуклый многоугольник с дырками (FIXME)
  • Много-точек
  • Много-линий
  • Много-многоугольников
  • Много-всякого
Вот как человек может вставить такие данные:

INSERT roads(the_geom) VALUES (ST_GeometryFromText('LINESTRING(0 0, 1 1, 2 3, 5 3, 10 8'));

А вот так можно их получить обратно:

SELECT ST_AsText(the_geom)  FROM roads;

Для компьютерной же обработки предусмотрены слегка другие функции:

INSERT roads(the_geom) VALUES (ST_GeomFromWKB(bytes));

А вот так можно их получить обратно:

SELECT ST_AsBinary(the_geom)  FROM roads;

Самым, конечно знаменитым проектом использующим GIS СУБД является openstreetmap (несмотря на xml обмен данными между базой и пользователями). В openstreetmap используется Postgresql/PostGIS.

Так вот этот PostGIS позволяет хранить не только 2d геометрические объекты, но как Вы уже догадались и 3d и 4d.

Для того чтобы Вы могли получить из СУБД Postgresql эти объекты к себе в common lisp, нужно просто сделать:

(require :postmodern)
(use-package :postmodern)

(require :cl-ewkb)
(use-package :cl-ewkb)

(decode (caar (query (:select (:ST_AsEWKB "LINESTRING(1 2 1, 2 3 4)")))))

Вуаля.

P.S. Ах да, asdf пакет-то находится по адресу: https://github.com/filonenko-mikhail/cl-ewkb

18 мая 2011

Common Lisp. Export struct.

Понадобился функционал структуры, на тебе пожалуйста: defstruct macro.

А вот теперь написали вы стуктуру:

(defstruct (point-primitive (:type (vector ieee754-double))
               (:constructor make-point-primitive (x y)))
    "2d point data - struct contains x y coordinates."
    (x 0.0d0 :type ieee754-double)
    (y 0.0d0 :type ieee754-double))

Данный макрос развернулся в конструктор, копирующую функцию, и методы доступа к членам структуры. Надо эти функции экспортировать. Вот простейший макрос для создания структуры, и экспорта ее из пакета.

(defmacro defstruct-and-export (structure &rest members)
  "Define a structure STRUCT with members MEMBERS and export the
   standard functions created. SPECIALS is a list of extra parameters eg
   ((:print-function pf)). Note double parentheses."
  (append
   `(progn
        ,(if (not (null members))
            (if (stringp (car members))
                `(defstruct ,structure ,(car members) ,@(cdr members))
                `(defstruct ,structure ,@members))
             `(defstruct ,structure))
        ,`(export ,`(quote ,(intern (concatenate 'string "MAKE-" (symbol-name (car structure))))))
        ,`(export ,`(quote ,(intern (concatenate 'string "COPY-" (symbol-name (car structure)))))))
      (if (not (null members))
          (if (stringp (car members))
              (mapcar  #'(lambda (member)
                             `(export ,`(quote ,(intern (concatenate 'string (symbol-name (car structure)) "-" (symbol-name (car member))))))) (cdr members))
              (mapcar  #'(lambda (member)
                             `(export ,`(quote ,(intern (concatenate 'string (symbol-name (car structure)) "-" (symbol-name (car member))))))) members)))
      (if (find :named structure)
          `((export ,`(quote ,(intern (concatenate 'string (symbol-name (car structure)) "-P" ))))
               (deftype ,(intern (symbol-name (car structure))) () '(satisfies ,(intern (concatenate 'string (symbol-name (car structure)) "-P" ))))))))

На вышеприведенном примере структуры макрос разворачивается так:

(PROGN
 (DEFSTRUCT
     (POINT-PRIMITIVE (:TYPE (VECTOR IEEE754-DOUBLE))
      (:CONSTRUCTOR MAKE-POINT-PRIMITIVE (X Y)))
   "2d point data - struct contains x y coordinates."
   (X 0.0d0 :TYPE IEEE754-DOUBLE)
   (Y 0.0d0 :TYPE IEEE754-DOUBLE))
 (EXPORT 'MAKE-POINT-PRIMITIVE)
 (EXPORT 'COPY-POINT-PRIMITIVE)
 (EXPORT 'POINT-PRIMITIVE-X)
 (EXPORT 'POINT-PRIMITIVE-Y))

17 мая 2011

Clojure, NoSql

Раз пошла такая пьянка, режь последний огурец. Я надеюсь Вас не смущает такое начало, тем более, что сегодня мы резко переместимся в сторону NoSQL. Почему резко? Да, потому что я только что впервые в жизни набрал в google "clojure NoSql", и этот пост пишу в режиме live.

После некоторых скитаний, оказываемся здесь: http://habrahabr.ru/blogs/nosql/77909/
Хм, ничего не понял. Будем как обычно основываться на интуиции. Итак couchDB или mongoDB? И тут ассоциативно: Erlang versus C++. А так как я знаю, что такое голосование долларом (рублём), то своим вниманием я голосую, конечно же против C++.
Erlang мне нравится, чрезвычайно интересный нишевый функциональный язык.


sudo apt-get install couchdb


http://127.0.0.1:5984/

Ага, работает.

Хорошо спрашиваем google "clojure couchdb". Ответил нам библиотекой [clojure-couchdb "0.4.5"]. Добавим ее в зависимости для проекта, выполним lein deps.

 Ну что же, давайте хранить сообщения блога теперь в couchdb. Для начала создадим базу данных. Сделаем это через clojure repl, в целях знакомства с clojre-couchdb api.


lein swank
emacs M+x slime-connect

Загрузим символы из couchdb.client


(use 'couchdb.client)


Какие у нас базы данных есть?


(database-list "http://localhost:5984/")


Создадим новую базу данных


(database-create "http://localhost:5984/" "blog")


Добавим туда документ


(document-create "http://localhost:5984/" "blog"  "post" {:title "New post" :body "My name is Michael. I live in Minsk. Minsk is the capital of Belarus."})

Фух, неплохо. На одном дыхании.

Небольшие технические подробности о couchDB. couchDB нереляционная документо-ориентированная база данных написанная на языке Erlang. API реализуется с помощью RESTful HTTP/JSON.

Управление документами осуществляется с помощью функций (document-* host database &optional id data)

Давайте получим обратно список документов:


(document-list "http://localhost:5984/" "blog")


Давайте получим содержимое документа:


(document-get "http://localhost:5984/" "blog" "post")

Давайте изменим текущий объект, по принципу "получить-смерджить-вставить":


(let [doc (document-get "http://localhost:5984/" "blog" "post")]
         (document-update "http://localhost:5984/" "blog" "post" (merge doc {:title "Not so new post"})))
 


Давайте удалим этот объект:


(document-delete "http://localhost:5984/" "blog" "post")

Всё что мне надо я узнал. Давайте внедрять. И на этом этапе шаблон model-view-pattern подходит нам как нельзя кстати, нам всего-то надо заменить модель.



Добавим адрес базы данных в файл src/clj_blog/system/db.clj


(def *couch-host* "http://127.0.0.1:5984/")
(def *couch-db* "post")


 Прежде чем перейти к модели, один подводный камень нас ожидает. Структура "таблицы", а точнее документа в couchDB будет слегка отличатся от структуры sql таблицы, поэтому нам необходимо будет изменить файлы шаблонов.

В файлах templates/posts/*.soy  нужно заменить все post.id на post._id
В файлах tempaltes/posts/*.soy нужно заменить textarea ... name ="text" на textarea ... name="body"


Создадим файл

src/clj_blog/models/m_couch_post.clj

следующего содержания




Меняем в контроллере использование m-post модели на m-couch-post


(:require [clj-blog.models.m-couch-post :as post]
  .....

Для просмотра базы данных можно просто использовать ссылку.

 http://127.0.0.1:5984/blog/

Размер исзодного кода модели уменьшился на 15%:) Это произошло засчет более простого API для работы с JSON документами, чем с SQL таблицами.


Compojure, Sessions

Дамы и господа, продолжим наши размышления о Clojure. Значит, если Вы здесь, то, что такое Сlojure Вам объяснять не надо.

Я хочу присоединиться к работе моего друга над небольшим Clojure сайтом. Скачав исходник по ссылке, мы получаем заготовку блога:

  • спроектированного по model-view-controller шаблону.
  • с системой html шаблонов от самого google - closure templates.
  • с системой хранилища данных mysql.

Зависимости у проекта небольшие и разрешаются одной командой: lein deps.

Я уже был готов дать команду:

lein please deploy this site to my home host 

но возник вопрос о механизме доступа к контенту сайта. Так как мир представлен инем и янем, то соответственно при рождении сайта рождаются те, кто хочет его погубить. Надо от них защищаться механизмами авторизации/аутентификации. А их пока нет. 

Приступим:

Создадим модель, которая будет отвечать на запросы контроллеру, касаемо аутентификации, авторизации. Модель нам понадобиться для того, чтобы например поддерживать несколько хранилищ пользовательских данных (своя база, openid, etc).

Создадим наш контроллер, который будет аутентифицировать (это когда логин и пароль вводиться), а также авторизировать (это когда с помощью нашего контроллера будет производится проверка доступности другого контроллера). 

Создадим таблицу содержащую список пользователей.

CREATE TABLE user (   id int(11) NOT NULL AUTO_INCREMENT,   email varchar(512) NOT NULL,   password varchar(512) NOT NULL,   fullname varchar(512),   PRIMARY KEY (id));

Создадим таблицу для хранения прав доступа к контроллерам.

CREATE TABLE user_right (
   user_id int(11) NOT NULL,
   controller varchar(128) NOT NULL,
   method varchar(128) NOT NULL,
 PRIMARY KEY (user_id, controller, method))


Добавим пользователя:


insert into user(email, password, fullname) values ('user@site.com', '1234', 'New User');

Теперь давайте добавим данному пользователю право на метод new контроллера post.


insert into user_right(user_id, controller, method, allow) values ((select id from user where email = 'user@site.com'), 'post', 'new', true);


Теперь давайте создадим модель, которая будет аутентифицировать и авторизировать.

Метод authentication возвращает 0 в случае неуспешной аутентификации и 1 в случае успешной. 

Метод authorisation возвращает 0 в случае отстутсвия доступа к методу контроллера и 1 в противном случае.

Теперь можно было бы перейти к контроллеру, если бы не одно НО. Наша программа не имеет состояния, чистая функциональщина. Для реального же мира состояние нужно. Хранить состояния мы будет в сессии. Доступ к сессии, мы сделаем с помощью ring.middleware.session. ring.middleware модули - это функции-прокси между request/responce и нашим web приложением.

В файле src/*/routes.clj включим дополнительный модуль и добавим прокси функцию.


Механизм работы с сессией таков. Теперь в request появиться ключ :session, который хранит в себе мапу. Если мы в нашем responce добавим в ключ :session добавим мапу, то она нам вернется при следующем request-е. Однако если мы в ключ :session запишем nil, сессия уничтожится.

Не все наши контроллеру обязательно возвращают :session. Поэтому мы сделаем еще одну функцию-прокси, которая будет сохранять мапу сессии, если контроллер не записал в нее nil. Код гораздо проще слов. Навешиваем middleware.



Определяем "сохранялку" сессии:


Для просмотра текущей сессии добавьте следующий путь для приложения:

(GET "/session" {session :session} (str session)) 

Теперь давайте определим контроллер, который будет производить аутентификацию (логин) и авторизацию (есть ли права).


Создадим для него шаблоны:

templates/app/login.soy


 templates/login/index.soy


Теперь давайте определим роуты, по которым будет происходить логин/выход:


Теперь давайте защитим какой-нибудь роут. Например, вот так:

(GET "/posts/new" {session :session} [] (if (= 1 (user-session/authorisation session "post" "new")) (post/new)   (redirect-to "/login")))


Ну теперь выполните:

#lein deps
 lein ring server 

Попробуйте создать пост. Вас перенаправит на страничку login. Залогиньтесь под следующим пользователем user@site.com с паролем 1234. Теперь попробуйте создать пост. Для просмотра сессии, наберите http://127.0.0.1/session .

 Постараюсь подвести итоги:
  • Мы добавили механизм сессии в сайт
  • Мы добавили модель для аутентификации и проверки доступа.
  • Мы создали контроллер, который предоставляет страницу для логина. Кроме того данный контроллер проверяет доступность объекта сайта для пользователя.