Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #
- # Дефинираме си модул, за да има име структурата, която ще използваме за потребители
- #
- defmodule User do
- #
- # Дефинираме си структурата и задаваме стойности по подразбиране.
- #
- defstruct username: '', password: '', email: '', sex: '', is_logged_in: false
- #
- # Презаписваме to_string метода, за да не изписваме всеки път обекта по възможно най-глупавият начин.
- # Спестява време
- #
- defimpl String.Chars, for: User do
- def to_string(user) do
- #
- # Връщаме многоредов string с информацията от структурата.
- #
- """
- Username: #{user.username}
- Password: #{user.password}
- Email: #{user.email}
- Sex: #{user.sex}
- Is Logged In: #{user.is_logged_in}
- """
- end
- end
- end
- #
- # Дефинираме си модул, който ще използва структура, която ще служи за следене на състоянието на приложението ни
- #
- defmodule AppState do
- #
- # В структурата имаме текущият потребител, който е влязъл и списък от потребители. Задаваме им стойности
- # по подразбиране.
- #
- defstruct currentUser: %User{}, users: []
- #
- # Презаписваме to_string метода, за да не изписваме всеки път обекта по възможно най-глупавият начин.
- # Спестява време
- #
- defimpl String.Chars, for: AppState do
- def to_string(state) do
- """
- Current User:
- Username: #{state.currentUser.username}
- Password: #{state.currentUser.password}
- Email: #{state.currentUser.email}
- Sex: #{state.currentUser.sex}
- Is Logged In: #{state.currentUser.is_logged_in}
- Total #{Enum.count(state.users)} users registered
- """
- end
- end
- end
- #
- # Дефинираме си модула за приложението ни, което ще използва GenServer, за да обработва заявки с име Database
- #
- defmodule Database do
- #
- # Казваме, че искаме да използваме GenServer имплементацията в нашият модул
- #
- use GenServer
- #
- # Създаваме си метод, чрез който да стартираме нашият сървър, като приема за текущо състояние
- # нашата структура за състояние, която си направихме - AppState
- #
- def start_link(), do: GenServer.start_link(__MODULE__, %AppState{})
- #
- # Създаваме си метод, чрез който да спираме нашият сървър с параметри на process id(pid), причина за спиране
- # на сървъра(:normal) и време за което да се спре (:infinity).
- #
- def stop(pid), do: GenServer.stop(pid, :normal, :infinity)
- #
- # Implementations
- #
- #
- # Създаваме си два метода за инициализация и за терминиране.
- # Извикват се, когато се стартира сървъра успешно и когато е извикан метода stop.
- #
- @impl true
- def init(users) do
- {:ok, users}
- end
- @impl true
- def terminate(_reason, _state) do
- :ok
- end
- #
- # Тук си създаваме методи, които клиента може да използва, за да управлява състоянието на приложението си.
- # Примерно: потребителят иска да си създаде акаунт - ще извика Database.create() и ще си въведе информацията.
- #
- #
- # Client Calls
- #
- #
- # Дефинираме си метод create, който ще използва handle_call, който се извиква чрез GenServer.call
- # Първият параметър на GenServer.call e process id(pid) на сървъра, който е стартиран.
- # Вторият параметър служи за подаване на специално име, което се използва да различава
- # заявките от различните GenServer.call-ове. Пример: За да разпознае сървъра ни, че искаме
- # да създадем нов потребител, то ние ще му кажем, че името, което ще използваме е :create и
- # ще използваме параметър с име user от неизвестен тип.
- #
- # Следователно в метода долу в handle_call трябва да му кажем, че този метод, който разписваме
- # като код ще използва това име и приема тези параметри, за да можем да обработим заявката,
- # която ще дойде.
- #
- # В следващите методи логиката е същата:
- # GenServer.call(номер на процеса, <уникално име на точката за достъп> или {<уникално име на точката за достъп>, <неограничен брой параметри>})
- #
- # Ако въведем само име на точката за достъп, както е в последният метод show, по този начин казваме, че
- # не искаме да имаме параметри.
- #
- def create(pid, user), do: GenServer.call(pid, {:create, user})
- def login(pid, username, password), do: GenServer.call(pid, {:login, username, password})
- def logout(pid, username), do: GenServer.call(pid, {:logout, username})
- def change_password(pid, username, current_password, new_password), do: GenServer.call(pid, {:change_password, username, current_password, new_password})
- def delete(pid, username), do: GenServer.call(pid, {:delete, username})
- def show(pid), do: GenServer.call(pid, :show)
- #
- # Callbacks
- #
- #
- # Тук е обработката на командата create от по-горе.
- # @impl true се използва, за да се каже, че ще използваме метода от GenServer-а.
- # Представи си го като наследяване. Няма как да не наследиш методи от интерфейс или абстрактен клас.
- # Може да се напише само на първият метод, който се разписва и нататък не е нужно да се пише.
- #
- # Обратно на метода. Приема три параметъра. Както казахме по-горе, имаме уникално име и подадени от
- # потребителя параметри. На всеки handle_call има още два параметъра.
- #
- # Първият параметър задължително е точката на обработване - <уникално име> или {<уникално име>, <n на брой параметри>}
- # Вторият параметър се използва от GenServer-а и чрез него ние можем да проследим кой е извикал този метод, ако имаме
- # няколко сървъра, които са разделени в клъстер.
- # Третият параметър е текущото състояние на приложението ни или AppState. Състоянието се подава, за да може да се
- # проследи какво се случва с обектите в приложението. Ако веднъж извикаме този метод и създадем потребител успешно,
- # то състоянието на приложението ни ще се обнови и ако извикаме който и да е следващ метод, то в състоянието на
- # приложението ни ще имаме в users масива един потребител, докато приложението ни не се затвори, ако не ги съхраняваме
- # във външен източник - база данни, текстов файл, json файл и други начини.
- #
- @impl true
- def handle_call({:create, user}, _from, state) do
- #
- # Проверяваме дали текущият ни потребител е влязъл в
- # акаунта си. По подразбиране горе в структурата is_logged_in
- # е false.
- #
- if not state.currentUser.is_logged_in do
- #
- # Ако е влязъл, искаме да проверим дали потребител с
- # подаденото потребителско име или имейл вече съществува
- # в списъка ни с потребители чрез помощта на груповият
- # метод Enum.any и ламбда функцията.
- #
- isUserExisting = Enum.any?(
- state.users,
- #
- # Създаваме си ламбда функцията, която се чете така:
- # За всеки потребител, изпълни тази функция, ако текущият
- # обект е с име на променливата localUser.
- #
- fn localUser ->
- localUser.username === user.username or
- localUser.email === user.email
- end
- )
- #
- # Ако съществува потребителят в списъка, извеждаме грешка
- # и подаваме текущото състояние напред за следващите
- # методи.
- #
- if isUserExisting do
- #
- # Връщаме отговор (това означава :reply, следователно
- # има и :noreply, ако искаме да не връщаме отговор),
- # в който казваме, че потребителят вече съществува.
- #
- {:reply, 'The user already exists', state}
- else
- #
- # В случай, че не съществува потребителя, добавяме го
- # към списъка с потребители и новият списък ще се върне като
- # резултат в updatedUsers променливата
- #
- # Създаваме си ново състояние на приложението, в което казваме
- # че искаме да подновим списъка с потребители с новият ни
- # потребител вътре в него, затова подаваме обновеният списък
- # на новото състояние като users параметър и използваме
- # старото състояние на текущият потребител, понеже не искаме
- # автоматично да си влиза в акаунта. Може да иска да прави още
- # потребители.
- #
- updatedUsers = [user|state.users]
- updatedState = %AppState{currentUser: state.currentUser, users: updatedUsers}
- #
- # Връщаме отговор с новото състояние на приложението ни
- #
- {:reply, updatedState, updatedState}
- end
- else
- #
- # Ако случайно се опитаме да направим потребител, докато сме
- # в някой друг потребител, ще ни върне този резултат.
- #
- {:reply, 'You are already logged in', state}
- end
- end
- #
- # Принципът на всички handle_call е същият както по-горе описаният.
- #
- def handle_call({:login, username, password}, _from, state) do
- #
- # Ако потребителят не е влязъл в профила си
- #
- if not state.currentUser.is_logged_in do
- #
- # Проверяваме дали потребителското име и паролата
- # съвпадат с някои от тези в списъка ни с потребители.
- #
- canUserLogIn = Enum.any?(
- state.users,
- fn user ->
- user.username === username or user.email === username
- and
- user.password === password
- end
- )
- #
- # Ако успешно намери потребител с такива данни
- #
- if canUserLogIn do
- #
- # Създаваме си една анонимна функция, която ще се
- # преизползва няколко пъти с цел по-изчистен код.
- #
- predicate = fn user -> user.username === username end
- #
- # Създаваме си map или tuple, който ще се използва за
- # съхранение на данни в две променливи
- #
- # Първата - user - ще се използва, за да се намери потребителя
- # по потребителско име и ще изпълни Enum.find(state.users, false, predicate).
- #
- # Втората - userIndex - ще се използва, за да се намери индекса на
- # потребителя в масива, за да можем да го заменим по-късно с обновената му
- # информация.
- #
- {user, userIndex} = {
- Enum.find(state.users, false, predicate),
- Enum.find_index(state.users, predicate)
- }
- #
- # Ако успешно е намерен потребителят
- #
- if user do
- #
- # Ще използваме информацията от старият потребител, но ще сменим статуса му
- # на is_logged_in от false на true, защото вече ще е влязъл в акаунта си.
- #
- updatedUser = %User{
- username: user.username,
- password: user.password,
- email: user.email,
- sex: user.sex,
- is_logged_in: true,
- }
- #
- # Обновяваме списъка ни с потребители с новият статус на нашият потребител
- #
- updatedList = List.replace_at(state.users, userIndex, updatedUser)
- #
- # Обновяваме и състоянието на приложението с намереният и обновеният
- # потребител и обновеният списък с потребители.
- #
- updatedState = %AppState{currentUser: updatedUser, users: updatedList}
- #
- # Връщаме отговор с обновеното състояние на приложението
- #
- {:reply, updatedState, updatedState}
- end
- else
- #
- # Ако случайно нещо се обърка и потребителят не е нацелил паролата
- # или потребителското си име, връщаме отговор със старото състояние
- # на приложението.
- #
- {:reply, 'Incorrect username/password', state}
- end
- else
- #
- # Ако случайно се опитаме да влезем в друг акаунт, но вече сме влезли,
- # връщаме тази грешка.
- #
- {:reply, 'You are already logged in', state}
- end
- end
- #
- # Принципът на всички handle_call е същият както по-горе описаният.
- #
- def handle_call({:logout, username}, _from, state) do
- #
- # Ако потребителят е влязъл в профила си и ако името му отговаря на
- # името на текущият потребител.
- #
- if state.currentUser.is_logged_in and username === state.currentUser.username do
- #
- # Създаваме си една анонимна функция, която ще се
- # преизползва няколко пъти с цел по-изчистен код.
- #
- predicate = fn user -> user.username === username end
- #
- # Създаваме си map или tuple, който ще се използва за
- # съхранение на данни в две променливи
- #
- # Първата - user - ще се използва, за да се намери потребителя
- # по потребителско име и ще изпълни Enum.find(state.users, false, predicate).
- #
- # Втората - userIndex - ще се използва, за да се намери индекса на
- # потребителя в масива, за да можем да го заменим по-късно с обновената му
- # информация.
- #
- {user, userIndex} = {
- Enum.find(state.users, false, predicate),
- Enum.find_index(state.users, predicate)
- }
- #
- # Ако успешно е намерен потребителят
- #
- if user do
- #
- # Ще използваме информацията от старият потребител, но ще сменим статуса му
- # на is_logged_in от true на false, защото вече ще е излязъл от акаунта си.
- #
- updatedUser = %User {
- username: user.username,
- password: user.password,
- email: user.email,
- sex: user.sex,
- is_logged_in: false,
- }
- #
- # Обновяваме списъка ни с потребители с новият статус на нашият потребител
- #
- updatedUsers = List.replace_at(state.users, userIndex, updatedUser)
- #
- # Обновяваме и състоянието на приложението с намереният и обновеният
- # потребител и обновеният списък с потребители.
- #
- updatedState = %AppState{currentUser: %User{}, users: updatedUsers}
- #
- # Връщаме отговор с обновеното състояние на приложението
- #
- {:reply, updatedState, updatedState}
- end
- else
- #
- # Ако случайно се опитаме да излезем от акаунта си, но още не сме влезли,
- # връщаме тази грешка.
- #
- {:reply, 'You are not logged in', state}
- end
- end
- #
- # Принципът на всички handle_call е същият както по-горе описаният.
- #
- def handle_call({:change_password, username, current_password, new_password}, _from, state) do
- #
- # Ако потребителят е влязъл в профила си и ако името му отговаря на
- # името на текущият потребител.
- #
- if state.currentUser.is_logged_in and username === state.currentUser.username do
- #
- # Създаваме си една анонимна функция, която ще се
- # преизползва няколко пъти с цел по-изчистен код.
- #
- predicate = fn user -> user.username === username end
- #
- # Създаваме си map или tuple, който ще се използва за
- # съхранение на данни в две променливи
- #
- # Първата - user - ще се използва, за да се намери потребителя
- # по потребителско име и ще изпълни Enum.find(state.users, false, predicate).
- #
- # Втората - userIndex - ще се използва, за да се намери индекса на
- # потребителя в масива, за да можем да го заменим по-късно с обновената му
- # информация.
- #
- {user, userIndex} = {
- Enum.find(state.users, false, predicate),
- Enum.find_index(state.users, predicate)
- }
- #
- # Ако успешно е намерен потребителят
- #
- if user do
- #
- # Правим си една променлива с проверка дали текущата парола отговаря на
- # потребителската и дали новата парола не е различна от текущата парола.
- #
- canChangePassword = user.password === current_password and new_password != current_password
- #
- # Ако проверката мине успешно и потребителя може да си смени паролата
- #
- if canChangePassword do
- #
- # Ще използваме информацията от старият потребител, но ще сменим паролата му
- # на новата парола.
- #
- updatedUser = %User{
- username: user.username,
- password: new_password, # Ето този ред
- email: user.email,
- sex: user.sex,
- is_logged_in: user.is_logged_in,
- }
- #
- # Обновяваме списъка ни с потребители с новата парола на нашият потребител
- #
- updatedUsers = List.replace_at(state.users, userIndex, updatedUser)
- #
- # Обновяваме и състоянието на приложението с намереният и обновеният
- # потребител и обновеният списък с потребители.
- #
- updatedState = %AppState{currentUser: updatedUser, users: updatedUsers}
- #
- # Връщаме отговор с обновеното състояние на приложението
- #
- {:reply, updatedState, updatedState}
- else
- #
- # Ако някой от критериите не отговаря на изискванията за смяна на
- # парола - новата парола е като старата или текущата парола не
- # отговаря на въведената, връщаме тази грешка.
- #
- {:reply, 'There was a problem trying to change your password', state}
- end
- end
- else
- #
- # Ако случайно се опитаме да си сменим паролата на акаунта, но още не сме влезли,
- # връщаме тази грешка.
- #
- {:reply, 'You are not logged in', state}
- end
- end
- #
- # Принципът на всички handle_call е същият както по-горе описаният.
- #
- def handle_call({:delete, username}, _from, state) do
- #
- # Ако потребителят е влязъл в профила си и ако името му отговаря на
- # името на текущият потребител.
- #
- if state.currentUser.is_logged_in and state.currentUser.username === username do
- #
- # Създаваме си една анонимна функция, която ще се
- # преизползва няколко пъти с цел по-изчистен код.
- #
- predicate = fn user -> user.username === username end
- #
- # Намираме потребителя от списъка с потребители
- # Ако не го намери, по подразбиране връща стойността
- # на вторият параметър - в случая false.
- #
- user = Enum.find(state.users, false, predicate)
- #
- # Ако потребителят е намерем и потребителското име отговаря на това на
- # текущият потребител
- #
- if user != false and user.username === state.currentUser.username do
- #
- # Изтриваме потребителят от списъка ни с потребители.
- # Методът връща новият списък без потребителя, който изтрихме.
- #
- updatedUsers = List.delete(state.users, user)
- #
- # Обновяваме и състоянието на приложението с празен потребител
- # (понеже си изтрихме акаунта и вече нямаме такъв, автоматично ни
- # изхвърля от акаунта) и обновеният списък с потребители.
- #
- updatedState = %AppState{currentUser: %User{}, users: updatedUsers}
- #
- # Връщаме отговор с обновеното състояние на приложението
- #
- {:reply, updatedState, updatedState}
- else
- #
- # Ако потребителското име не отговря, това не е нашият акаунт, следователно
- # връщаме тази грешка.
- #
- {:reply, 'Log into your own account', state}
- end
- else
- #
- # Ако случайно се опитаме да си изтрием акаунта, но още не сме влезли
- # или ако се опитваме да изтрием акаунта на друг потребител,
- # връщаме тази грешка.
- #
- {:reply, 'You are not logged in or trying to delete other user', state}
- end
- end
- #
- # Принципът на всички handle_call е същият както по-горе описаният.
- # На този handle_call нямаме параметри, а само уникално име. Параметри
- # не ни трябват, понеже ще визуализираме потребителите от състоянието на
- # приложението ни (state).
- #
- def handle_call(:show, _from, state) do
- #
- # Връщаме отговор с текущото състояние на приложението ни
- # Вътре в състоянието има текущ потребител и списък от потребители
- #
- {:reply, state, state}
- end
- end
- #
- # Дефинираме основният модул на приложението ни
- #
- defmodule Main do
- #
- # Дефинираме си и основната функция, която ще се изпълни при стартиране на
- # приложението ни.
- #
- def main do
- #
- # Използваме метода от GenServer за стартиране на сървъра.
- # При успешно стартиране, той ни връща статус :OK, че всичко е наред
- # и идентификатор на процеса(process id или pid), който да използваме
- # за нашите методи.
- #
- {:ok, pid} = Database.start_link()
- #
- # Създаваме си два потребителя за пробата с фективна информация
- #
- user = %User{
- username: 'Kolev',
- password: 'sample',
- email: 'kolev@mypos.com',
- sex: 'Male',
- }
- user2 = %User{
- username: 'Kolev2',
- password: 'sample',
- email: 'kolev2@mypos.com',
- sex: 'Male',
- }
- #
- # Използваме нашите методи, които си направихме, за да създадем
- # двата потребителя в нашето приложение и да ги попълним в състоянието
- # на приложението ни.
- #
- Database.create(pid, user)
- Database.create(pid, user2)
- #
- # Пробваме да влезем в един от потребителите
- # Може да се види дали е успешно или има грешка ако преди
- # метода се напише IO.puts, както е при logout-а отдолу.
- #
- Database.login(pid, 'Kolev', 'sample')
- #
- # Пробваме да излезем от акаунта ни.
- #
- IO.puts Database.logout(pid, 'Kolev')
- #
- # За онагледяване на промяната, използваме няколко реда print за преди и след промяна.
- #
- IO.puts ''
- IO.puts 'Before change'
- IO.puts ''
- #
- # Пробваме да влезем в един от потребителите
- #
- IO.puts Database.login(pid, 'Kolev2', 'sample')
- #
- # За онагледяване на промяната, използваме няколко реда print за преди и след промяна.
- #
- IO.puts ''
- IO.puts 'After change'
- IO.puts ''
- #
- # Пробваме да сменим паролата на един от потребителите и извеждаме резултата
- #
- IO.puts Database.change_password(pid, 'Kolev2', 'sample', 'sample2')
- #
- # За онагледяване на промяната, използваме няколко реда print за преди и след промяна.
- #
- IO.puts ''
- IO.puts 'All Users'
- IO.puts ''
- #
- # Използваме метода show, за да вземем всички потребители от списъка
- #
- Enum.map(Database.show(pid).users, fn user -> IO.puts user end)
- #
- # За онагледяване на промяната, използваме няколко реда print за преди и след промяна.
- #
- IO.puts ''
- IO.puts 'All Users after delete'
- IO.puts ''
- #
- # Изтриваме един потребител от списъка ни
- #
- Database.delete(pid, 'Kolev2')
- #
- # Използваме метода show, за да вземем всички потребители от списъка
- #
- Enum.map(Database.show(pid).users, fn user -> IO.puts user end)
- #
- # Спираме GenServer-а ни.
- #
- Database.stop(pid)
- end
- end
- #
- # Извикваме main метода от модула Main, за да можем да стартираме
- # приложението успешно
- #
- Main.main()
Add Comment
Please, Sign In to add comment