unam3 / fsd-rest-news Goto Github PK
View Code? Open in Web Editor NEWrest news api that emits json
License: BSD 3-Clause "New" or "Revised" License
rest news api that emits json
License: BSD 3-Clause "New" or "Revised" License
Нетотальные не стоит использовать, а все исключительные ситуации стоит обрабатывать. head
, last
, init
, tail
, fromJust
, etc
https://rizzoma.com/topic/c27faf1cfa188c1120f59af4c35e6099/0_b_9n8n_8jl2r/
Весь код отформатирован через hindent.
Вместо hindent можно использовать любой форматтер: brittany, ormolu, stylish-haskell и др.
В некоторых, причем довольно важных местах встречаются json объекты в уже сериализованном виде, например:
src/RestNews/DB/ProcessRequest.hs|732| then Just "{\"error\": \"\\\"offset\\\" must not be negative\"}"
src/RestNews/DB/ProcessRequest.hs|750| then Just "{\"error\": \"\\\"offset\\\" must not be negative\"}"
src/RestNews/DB/ProcessRequest.hs|768| then Just "{\"error\": \"\\\"offset\\\" must not be negative\"}"
Так делать не стоит, это не очень typeful. Минимум надо строить Value
, а потом сериализовавать с помощью encode
(или чего подобного), а хорошо бы еще тип этой ошибки иметь, это же:
newtype Error = Error { error :: Text }
для него можно задерайвить ToJSON
и уже возвращать более высокоуровневый тип, а не строку (тело запроса)
WARNING: Ignoring rest-news's bounds on wai-extra (==3.1.6); using wai-extra-3.0.29.2.
Line 201 in 325121c
Функция read
частичная: если не может распарсить строку, она кидает эксепшен ErrorCall
с параметром-строкой вида read: no parse
, который трудно перехватить и обработать, так как надо привязываться с конкретному тексту ошибки, который может быть каким угодно, библиотека не гарантирует его неизменность. В итоге юзер получит ошибку 500, хотя предпочтительнее обработать этот случай как отсутствие авторизации, и в ряде случаев запрос юзера получится обработать, несмотря на поврежденную сессию.
Лучше использовать readMaybe
, который никаких эксепшенов не кидает, а возвращает Nothing
при ошибке разбора. Возможно, будет проще парсить айди юзера в число там же, где строка с юзером добывается из сессии, и передавать дальше не строку, а Maybe UserId
.
Многие скрипты возвращают 404, например, эти:
users_getUser
tags_getTag
tags_createTag
categories_createCategory
Ответы неоднородные, некоторые возвращают просто строку "No such endpoint", некоторые в теле ничего не возвращают. В идеале бы чтобы все энды возвращали Error json тип, раз уж он есть (например tags_getTag
так делает)
Сюда буду скидывать мелкие замечания. Они все достаточно важные в плане читабельности и type safety, но их легко научиться править и не допускать в будущем, так что я уверен, у тебя с ними никаких проблем не будет. Править их необязательно, по желанию.
createUser
, getUser
и get_article
содержат продублированные имена полей юзера, которые отдаются в JSON, наподобие avatar
или user_id
. То же касается других сущностей. Если мы захотим переименовать поле, добавить или удалить, то придется разыскать и исправить много мест в коде одинаково. Это трудоемко и хорошо генерирует баги. Должна быть single point of truth - единственное место в коде, которое знает, как преобразовать юзера в JSON, включая все его имена полей. Имена полей в API и имена столбцов в базе, вообще говоря, не должны быть жестко связаны - у сервера может поддерживаться куча версий API, но мы же не будет держать несколько баз с разной схемой.
Возможно, это придется долго править, так что лучше отложить это на потом.
В нескольких файлах реализован ничего не делающий Handle Pattern: в RestNews.Middleware.Sessions, RestNews.DBConnection, RestNews.WAI. Еще в логгере, но там ситуация получше, ниже объясню. Возьмем для примера RestNews.Middleware.Sessions.
Config
и Handle
идентичны по сигнатуре полей, так что функция withSessions
фактически передает содержимое конфига (скопированное в хендл) в свой аргумент, т.е. ее, грубо говоря, можно упростить до withSessions = flip ($)
. RestNews.hs
передает сюда конфиг с реализацией и сам же пользуется содержимым конфига, замаскированным под хендл. Никакой другой код не пользуется хендлом. Я думаю, все эти файлы с хендлом не нужны (логгер нужен), потому что они не изолируют интерфейс от реализации.
Вот приблизительно каким должен быть хендл-паттерн (в одном из его вариантов, самый архитектурно полезный):
Логгер устроен так же, как те три файла, но его можно доработать и сделать полезным:
withLogger
, конфиг) - это реализация, ее нужно перенести в другой модуль, например, RestNews.Logger.Impl
.cDebug
должна быть прямо в модуле реализации, их не надо сюда передавать из RestNews
.Правда, RestNews все же не будет полностью независим от логгера, потому что ему придется вызвать реализацию withLogger
, но от RestNews в будущем можно бы было отделить специальный модуль настройки, где и содержались бы все эти вызовы типа withLogger
, и вот тогда RestNews стал бы точно независим (сейчас делать так не надо). И он не будет знать, какая библиотека логирования используется, что хорошо.
Сейчас в конфиге можно выбрать юзернейм, пароль, бднейм. Но при запуске тестов они падают, потому что конфиг захардкожен в
fsd-rest-news/test/RestNewsSpec.hs
Lines 297 to 305 in 9d1e93c
PATCH /categories
, боди {"category_id": 2, "name": "pluh_pattched", "parent_id": 2}
При редактировании категории нужно проверять, что в базе не образуются циклы, т.е. что новый родитель не является самой этой категорией или любым ее потомком (не только дочерней, а на любой глубине иерархии). Иначе зациклится или сломается логика выдачи категории (если выдавать ее целиком, со всеми родителями) и логика проверки категории в поиске.
Эти проверки вряд ли получится сделать целиком в базе, нужно будет сделать логику в коде и, может быть, появится желание порефакторить. В этом и смысл - если у нас все делает база, нет особого смысла писать проект на хаскеле :). Хаскель хорош там, где много логики.
Чистые тесты необходимы для того чтобы научиться абстрагироваться от имплементации и понять как можно прокинуть моковое окружение для тестирования.
Можно попробовать использовать Service Handle паттерн, или mtl-style паттерн проектирования, чтобы потестировать основной функционал сервера.
Ф-я restAPI
содержит PC.endpointToEitherSessionName
и runSession
, обе они вызывают Aeson'овский decode
. В целом restAPI
выглядит очень перегруженно, непонятно что вообще происходит
PATCH /categories
, тело запроса {"category_id": 2, "parent_id": 2}
Wrong parameters/parameters values
Можно отредактировать одновременно и имя категории, и родителя, но нет возможности отредактировать по отдельности, опустив поле, которое мы не хотим менять. Такая возможность должна быть. Иначе перед редактированием придется сначала запрашивать категорию, это один лишний запрос для клиента и риск race conditions в условиях, когда множество клиентов API правит категорию параллельно.
Это не получится целиком возложить на базу, понадобится правка логики в хаскеле.
Сейчас в качестве кодов ошибок бд используется строка вида "22001" "2201X" "23503" "23505", что совершенно нечитабельно. В зависимости от этих кодов выполняются разные ветки:
, case getErrorCode sessionResults of
Just "23503" -> Just "{\"error\": \"such user does not exist\"}"
Just "23505" ->
Just "{\"error\": \"such user is already an author\"}"
_ -> Nothing)
Почему бы не сделать ADT тип с читабельными названиями?
P.S. почему последняя строка _ -> Nothing
? Что вернется в ответе в этом случае?
GET-запросы, например, получение категории, передают параметры запроса в теле. По стандарту HTTP сервер должен игнорировать тело в GET-запросах, поэтому его нельзя использовать для передачи параметров. Нужно передавать их через URI query (/categories?id=1
) или через путь (/categories/1
). Идентификатор сущности предпочтительнее передавать через путь, но если трудно, то и квери сойдет. Остальные параметры, если они есть, нормально передавать в квери.
Я вроде бы не встречал, чтобы в проекте использовались квери-параметры (правда, целиком не успел проверить). Это важный способ передачи параметров и его надо освоить, в реальных проектах это точно будет.
Вноси пожалуйста правки через пулл-реквест, чтобы легче было отслеживать.
Ошибки, которые содержаться в RestNews.DB.Errors
вида:
newtype MustNotBeNegative =
MustNotBeNegative {error :: Text}
deriving (Show)
instance ToJSON MustNotBeNegative where
toJSON (MustNotBeNegative error')
= object ["name" .= error']
Не особо имеют смысла. Они все одинаковой структуры (имею ввиду типы, которые ньютайпы над текстом), так же они сразу же используются вместе с encode
в других местах. Почему нельзя иметь один тип ошибки конечной жсон ошибки?
type Error = Error {errror :: Text}
P.S. Почему такой странный инстанс ToJSON
? Почему поле name
, хотя в типе это error
? Если там действительно нужен name
почему бы не использовать его в типе и просто генерить инстанс?
Или, возможно, я проглядел. Сортировка без фильтра не слишком полезна, если новостей на сервере сотни тысяч. Для того мы и используем фильтр, чтобы не тратить ресурсы на обработку ненужных данных (чтение из базы, передача по сети, разбор и анализ). Фильтр и сортировка ортогональны, задаются в разных частях sql-запроса, так что сделать несложно.
Чтобы оба параметра могли задаваться в запросе, можно сортировку передавать параметром в квери, например /articles/tag?sort=date
.
В ProcessRequest некоторые ошибки определяются разбором человекочитаемого текста из постгреса:
Человекочитаемые тексты не предназначены для машинного разбора, они могут отличаться в разных версиях постгреса или даже в одной версии - постгрес не гарантирует, что для определенного SQLSTATE текст ошибки всегда соответствует одному шаблону. Опираться можно только на код ошибки SQLSTATE и еще, возможно, на поля вроде PG_DIAG_TABLE_NAME. Проблема с PG_DIAG_TABLE_NAME в том, что hasql их не передает, и постгрес не гарантирует их наличие, и я никогда ими не пользовался и не уверен, что оно обозначает таблицу-цель foreign key constraint, а не таблицу, где произошла ошибка. Возможно, понадобится сделать проверочные запросы.
В RestNews одна ошибка тоже проверяется по текстовому сообщению:
Line 225 in 325121c
"NO_SUCH_CATEGORY"
, а не прямо сообщение для юзера. ADT в хаскеле здесь будут еще удобнее.Можешь исправить все согласно HLint? Несмотря на то что это не варнинги, это в 95% очень хорошие советы по упрощению кода. Сейчас очень много всего лишнего что мешает читабельности, о чем hlint и говорит
В проекте часто используются строки для обозначения кодов ошибок и эндпойнтов (сессий), которые не являются произвольным текстом. Для них лучше создать новые типы, ниже объясню зачем:
data Endpoint = Auth | CreateUser | GetUser ...
. Имя session меня сначала запутало, сессия это все же другое понятие - стейт соединения, контекстные данные, хранимые для одного юзера между разными запросами. wai-session
работает именно с такими сессиями.getSessionName
возвращает Either String String
, где первый String это перекодированная в строку ошибка - лучше Either Error ...
. Ошибка это более высокоуровневый тип и с ним легче работать, а вызывающий код в состоянии перевести его в строку, если ему надо.newtype HasqlError = HasqlError String
.data ErrorForUser = FieldIsTooLong String | NoSuchEndpoint | ReferencedEntityDoesNotExist String | ...
. Переводить его в строку или в JSON можно на выходе из обработчиков, где генерируется ответ с ошибкой. FieldIsTooLong
содержит простое строковое поле - "username/category name/...", тут не думаю, что нужен отдельный ADT, как и для ReferencedEntityDoesNotExist
, где передается переведенный в строку айдишник.HasqlSessionResults
стоит переделать в Either HandlerError a
, где data HandlerError = MkUserError UserError | MkUnhandledError UnhandledError
. Вложенные Either неудобны - надо в правильном порядке писать лефты и райты, вкладывать fmap, а если понадобится третья разновидность ошибки, то ведь не будешь третий Either вкладывать и потом переделывать все 100500 мест, где создаются значения HasqlSessionResults
. По смыслу это значение является ошибкой или успехом - для этого хорош Either, а ошибка бывает нескольких видов и ее мы делаем отдельным типом.Почему лучше создавать отдельные типы:
case
не понадобится кейс _ ->
, и это упростит код: не надо обрабатывать невозможный случай, и где-нибудь из результата функции исчезнет Maybe
или какой-нибудь UnknownError
. Легко добавлять новые конструкторы в ADT: компилятор выдаст варнинги во всех местах с паттерн-матчингом, где надо будет доработать код (если там нет паттерна _
).++
, length
, words
) либо этот смысл неоднозначен. Когда мы создадим отдельный тип, эти операции станет невозможно использовать по случайности, компилятор не пропустит. А если мы точно хотим превратить значение в строку, то можем сделать это явно и недвусмысленно, вызовом функции преобразования (которых может быть несколько, на любой вкус, и чья реализация может быть какой угодно).Когда программист использует стринг для ошибки, он все равно различает в голове просто строки и строки с ошибкой, фактически для него это разные типы, их просто надо сделать явными. Тогда баги за нас будет ловить компилятор, и коллегам будет проще разобраться. В хаскеле типы создаются очень легко и используются очень часто.
https://rizzoma.com/topic/c27faf1cfa188c1120f59af4c35e6099/0_b_9n8n_8jl2r/
Весь проект скомпилирован с флагами -Wall и -Werror и нет ни одной ошибки и ни одного варнинга от компилятора.
Довольно часты конструкции вида
getArticlesSortedByCategory sessionRun connection request =
let params = offset (request :: OffsetRequest)
in do sessionResults <- ...
Это увеличивает индентацию более чем на 1 отступ. Тем временем let
байндинги можно писать внутри do
блока:
getArticlesSortedByCategory sessionRun connection request = do
let params = offset (request :: OffsetRequest)
sessionResults <- ...
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.