rtems api用户指南
Elixir代表了相对较新的编程语言,面向更广泛的受众。 它于2011年发布,此后一直在开发中。 他的主要特征是取消功能范式,因为它建立在Erlang之上并在BEAM(Erlang VM)上运行。
Elixir专为构建快速,可扩展和可维护的应用程序而设计,而使用Phoenix可以在Web环境中开发这些应用程序。 Phoenix是用Elixir编写的Web框架,它从流行的框架(例如Python的Django或Ruby on Rails)中汲取了很多概念。 如果您熟悉这些,那将是一个不错的起点。
Elixir / Phoenix是很好的组合,但是在开始编写应用程序之前,那些不熟悉所有概念的人应该首先阅读以下文档。
Elixir随附Mix,它是内置工具,可帮助编译,生成和测试应用程序,获取依赖项等。
我们通过运行来创建我们的应用程序
mix phx.new company_api
这告诉mix创建名为company_api的新Phenix应用。 运行此指令后,将创建应用程序结构:
bash
* creating company_api/config/config.exs
* creating company_api/config/dev.exs
* creating company_api/config/prod.exs
* creating company_api/config/prod.secret.exs
* creating company_api/config/test.exs
* creating company_api/lib / company_api / application . ex
* creating company_api/ lib / company_api . ex
* creating company_api/ lib / company_api_web / channels / user_socket . ex
* creating company_api/ lib / company_api_web / views / error_helpers . ex
* creating company_api/ lib / company_api_web / views / error_view . ex
* creating company_api/ lib / company_api_web / endpoint . ex
* creating company_api/ lib / company_api_web / router . ex
* creating company_api/ lib / company_api_web . ex
* creating company_api/mix.exs
* creating company_api/README.md
* creating company_api/test/support/channel_case.ex
* creating company_api/test/support/conn_case.ex
* creating company_api/test/test_helper.exs
* creating company_api/test/company_api_web/views/error_view_test.exs
* creating company_api/ lib / company_api_web / gettext . ex
* creating company_api/priv/gettext/en/LC_MESSAGES/errors.po
* creating company_api/priv/gettext/errors.pot
* creating company_api/ lib / company_api / repo . ex
* creating company_api/priv/repo/seeds.exs
* creating company_api/test/support/data_case.ex
* creating company_api/ lib / company_api_web / controllers / page_controller . ex
* creating company_api/ lib / company_api_web / templates / layout / app . html . eex
* creating company_api/ lib / company_api_web / templates / page / index . html . eex
* creating company_api/ lib / company_api_web / views / layout_view . ex
* creating company_api/ lib / company_api_web / views / page_view . ex
* creating company_api/test/company_api_web/controllers/page_controller_test.exs
* creating company_api/test/company_api_web/views/layout_view_test.exs
* creating company_api/test/company_api_web/views/page_view_test.exs
* creating company_api/.gitignore
* creating company_api/assets/brunch-config.js
* creating company_api/assets/css/app.css
* creating company_api/assets/css/phoenix.css
* creating company_api/assets/js/app.js
* creating company_api/assets/js/socket.js
* creating company_api/assets/package.json
* creating company_api/assets/static/robots.txt
* creating company_api/assets/static/images/phoenix.png
* creating company_api/assets/static/favicon.ico
如果出现提示,请安装其他依赖项。 接下来,我们需要配置数据库。 在此示例中,我们使用了PostgreSQL,通常Phoenix与该DBMS的集成最佳。
打开/config/dev.exs和/config/test.exs并设置用户名,密码和数据库名称。 设置数据库后,运行
mix ecto .create
这将创建开发和测试数据库,之后
mix phx.server
这应该在默认端口4000上启动服务器(Cowboy)。在浏览器中进行检查,如果看到的是登陆页面,则设置很好。 所有配置都放在/config/config.exs文件中。
在编码之前,将解释开发的几个部分:
请注意,以下部分不会针对整个应用程序进行描述,但是您会明白的。
在开发过程中,我们想编写干净的代码,并且还要考虑规范以及代码在实现之前需要做什么。 这就是为什么我们使用TDD方法。
首先在目录test / company_api_web /中创建模型目录,然后创建user_test.exs。 之后,创建一个模块:
defmodule CompanyApiWeb.UserTest do
use CompanyApi.DataCase, async: true
end
在第二行中,我们使用宏用法注入一些外部代码,在这种情况下,将data_case.exs脚本放置在test / support /目录中以及其他脚本中,并使用`async: true`
来表示该测试将与其他测试异步运行。 但是要小心,如果测试将数据写入数据库或在某种意义上更改了某些数据,则它不应运行asyc。
想想应该测试什么。 在这种情况下,让我们测试使用有效和无效数据创建用户。 可以通过模块属性将某些模拟数据设置为常量,例如:
@valid_attributes %{ name: "John" ,
subname: "Doe" ,
email: "doe@gmail.com" ,
job: "engineer"
}
当然,您不必使用模块属性,但这可以使代码更简洁。 接下来让我们编写测试。
test"user with valid attributes" do
user = CompanyApiWeb.User.reg_changeset(%User{}, @valid_attributes )
assert user.valid?
end
在此测试中,我们尝试通过调用方法reg_changeset / 2并声明为真值来创建变更集 。
如果我们用
mixtest test /company_api_web/models/user_test.exs
考试当然会失败。 首先,我们甚至没有用户模块,但是我们甚至没有数据库中的用户表。
接下来,我们需要编写一个迁移。
mix ecto.gen .migration create_user
生成priv / repo / migrations /中的迁移 。 在那里,我们使用Sugar Elixir语法定义了用于表创建的函数,然后将其转换为适当SQL查询。
def change do
create table( :users ) do
add :name , :varchar
add :subname , :varchar
add :email , :varchar
add :job , :varchar
add :password , :varchar
timestamps()
end
end
函数create / 2从函数table / 2返回的结构中创建数据库表。 有关字段类型,选项和创建索引的详细信息,请阅读docs。 默认情况下,将为每个表生成代理键,名称为id,类型为整数(如果未另外定义)。
现在我们运行命令
mix ecto .migrate
运行迁移。 接下来,我们需要创建模型,因此在lib / company_api_web /中创建models目录,并创建user.ex文件。 我们的模型用于表示数据库表中的数据,因为它将数据映射到Elixir结构中。
defmodule CompanyApiWeb.User do
use CompanyApiWeb, :model
schema "users" do
field :name , :string
field :subname , :string
field :email , :string
field :password , :string
field :job , :string
end
def reg_changeset (changeset, params \\ %{}) do
changeset
|> cast(params, [ :name , :subname , :email , :job , :password ])
|> validate_required([ :name , :subname , :email , :job ])
|> validate_format( :email , ~r/\S+@\S+\.\S+ /)
end
end
在第2行,我们使用lib / company_api_web / company_api_web.ex中定义的帮助程序,该帮助程序实际上会导入所有必要的模块以创建模型。 如果打开文件,您会看到模型实际上是一个函数,与控制器,视图,通道,路由器等相同。(如果没有模型函数,则可以自己添加)。
两种重要的方法是模式(表<->结构映射)和changeset / 2 。 Changeset函数不是必需的,但是Elixir的方法就是创建修改数据库的结构。 我们可以定义一个用于注册,登录等。所有验证和关联检查都可以在我们尝试将数据插入数据库之前完成。
有关更多详细信息,请查看Ecto.Changeset文档。 如果我们现在再次运行测试,它将通过。 根据需要添加任意数量的测试用例,并尝试覆盖所有边缘用例。
这应该包含简单模型的创建。 添加关联将在前面提到。
测试控制器与测试模型同等重要。 我们将测试新用户的注册,并让所有注册用户进入系统。 再次,我们在test / company_api_web / controllers /中创建名称为user_controller_test.exs的测试。 通过控制器测试,我们将使用conn_case.exs脚本。 在测试模型时(因为我们不需要),测试中没有提到的另一重要事项是设置块。
setupdo
user =
%User{}
|> User.reg_changeset( @user )
|> Repo.insert!
conn =
build_conn()
|> put_req_header( "accept" , "application/json" )
%{ conn: conn, user: user}
end
在调用每个测试用例之前,将调用Setup块,并且在此块中,我们准备用于测试的数据。 我们可以以元组或映射的形式从块中返回数据。 在此块中,我们将一个用户插入数据库并创建连接结构(即连接模型)。 同样,常量可用于设置数据。
@valid_data %{ name: "Jim" ,
subname: "Doe" ,
email: "doe@gmail.com" ,
job: "CEO"
}
@user %{ name: "John" ,
subname: "Doe" ,
email: "doe@gmail.com" ,
job: "engineer"
}
@user_jane %{ name: "Jane" ,
subname: "Doe" ,
email: "jane@gmail.com" ,
job: "architect"
}
现在,让我们编写测试,以发送创建新用户的请求。 服务器应处理请求,生成密码,创建新用户,使用生成的密码发送电子邮件并将用户作为json返回。 听起来很多,但我们会慢慢进行。 请注意,您应该尝试涵盖所有“路径”和边缘情况。 首先让我们先测试有效数据,然后再测试无效数据。
describe"tries to create and render" do
test "user with valid data" , %{ conn: conn} do
response =
post(conn, user_path(conn, :create ), user: @valid_data )
|> json_response( 201 )
assert Repo.get_by(User, name: "Jim" )
assert_delivered_email Email.create_mail(response[ "password" ], response[ "email" ])
end
test "user with invalid data" , %{ conn: conn} do
response =
post(conn, user_path(conn, :create ), user: %{})
|> json_response( 422 )
assert response[ "errors" ] != %{}
end
end
每个测试将发布请求发送到特定路径,然后我们检查json响应和断言值。
运行此测试
mixtest test /company_api_web/controller/user_controller_test.exs
会导致错误。 我们没有user_path / 3函数,这意味着未定义路由。 打开lib / company_api_web / router.ex 。 我们将添加范围“ / api”,它将通过:api管道。 我们可以将路由定义为资源,单独或嵌套路由。 定义这样的新资源:
resources "/users", UserController,only : [: index , : create ]
这样,Phoenix将创建路由,这些路由映射到索引并创建函数并由UserController处理。 如果打开控制台并键入
mix phx .routes
您可以看到路线列表,其中有user_path路线,一条路线带动词GET,另一条带动词POST。 现在,如果我们再次运行测试,这一次我们将得到另一个错误,缺少创建函数。 原因是我们没有UserController。 在lib / company_api_web / controllers中添加user_controller.ex。
现在定义新模块:
defmodule CompanyApiWeb.UserController do
use CompanyApiWeb, :controller
end
接下来,我们需要创建那个create / 2函数。 Create函数必须接受conn struct(并返回它)和params。 参数是结构,它承载浏览器提供的所有数据。 我们可以使用Elixir的一项强大功能,即模式匹配,将我们所需的数据与变量进行匹配。
def create (conn, %{ "user" => user_data}) do
params = Map.put(user_data, "password" , User.generate_password())
case Repo.insert(User.reg_changeset(%User{}, params)) do
{ :ok , user} ->
conn
|> put_status( :created )
|> render( "create.json" , user: user)
{ :error , user} ->
conn
|> put_status( :unprocessable_entity )
|> render( "error.json" , user: user)
end
end
在我们的测试中,我们通过post方法的params 用户user @valid_data发送该数据将与user_data匹配。 在用户模型中,定义generate_password函数,因此我们可以为每个新用户生成随机密码。
def generate_password do
:crypto .strong_rand_bytes( @pass_length )
|> Base.encode64
|> binary_part( 0 , @pass_length )
end
根据需要设置密码的长度。 由于user_data是一个映射,我们将使用键“ password”将新生成的密码放入该映射内。
尽管Elixir具有try / rescue块,但很少使用它们。 通常,大小写和模式匹配的组合用于错误处理。 函数insert(注意,我们不会使用insert!函数,因为它引发异常)返回两个元组之一:
{:ok , Ecto.Schema.t}
{ :error , Ecto.Changeset.t}
基于返回的元组,我们发送适当的响应。 由于我们正在制作JSON API,因此我们应该以json格式返回数据。 从控制器返回的所有数据均由适当的视图处理。 如果我们再次运行测试,将会得到另一个错误。 我们需要做的最后一件事是添加视图文件。 在lib / company_api_web / views /中创建user_view.ex文件,并在其中定义新模块和呈现方法。
defmodule CompanyApiWeb.UserView do
use CompanyApiWeb, :view
def render ( "create.json" , %{ user: user}) do
render_one(user, CompanyApiWeb.UserView, "user.json" )
end
def render ( "error.json" , %{ user: user}) do
%{ errors: translate_errors(user)}
end
def render ( "user.json" , %{ user: user}) do
%{ id: user.id,
name: user.name,
subname: user.subname,
password: user.password,
email: user.email,
job: user.job}
end
defp translate_errors (user) do
Ecto.Changeset.traverse_errors(user, &translate_error/ 1 )
end
end
首先从控制器调用render方法,在该方法中我们将调用键,视图模块和模板名称传递给render_one / 3 ,因此我们可以对match方法进行模式化。 现在,我们返回将要编码为json的数据。 我们不必调用render_one / 3方法,我们可以立即返回json,但这更加方便。
第二个render方法以json的形式呈现changeset结构提供的错误。 内置方法Ecto.Changeset.traverse_errors / 2从changeet.errors结构中提取错误字符串。
如果我们删除断言表明已发送电子邮件的那一行,则测试将通过。 这样就完善了我们测试和编写控制器的方式。 现在,您可以测试和编写索引方法,并添加涵盖更多代码的更多测试用例。
Elixir中有几个电子邮件库,但是在这个项目中,我们决定使用Bamboo 。 初始设置后,其用法相当简单。 打开mix.exs文件,并在deps函数下添加以下行:
{:bamboo , "~> 0.8" }
然后运行以下命令:
mix deps.get
这将下载依赖项。 之后,在应用程序功能中将Bamboo添加为extra_application。
在全局配置文件中,添加Bamboo的配置:
config:company_api , CompanyApi.Mailer,
adapter: Bamboo.LocalAdapter
在这里,我们使用Bamboo.LocalAdapter,但也有其他适配器。 现在,创建模块CompanyApi.Mailer和以下行:
use Bamboo.Mailer, otp_app: :company_api
在使用mailer之前,我们应该定义电子邮件结构。 添加到模型目录中的Email.ex文件(请注意,您应该先编写测试文件,然后添加文件,但我们现在将跳过该文件)。
defmodule CompanyApiWeb.Email do
import Bamboo.Email
def create_mail (password, email) do
new_email()
|> to(email)
|> from( "company@gmail.com" )
|> subject( "Generated password" )
|> html_body( "<h1>Welcome to Chat</h1>" )
|> text_body( "Welcome. This is your generated password #{password} . You can change it anytime." )
end
end
函数create_mail / 2返回我们将用于发送的电子邮件结构。 在运行测试之前,我们需要在/config/test.exs中添加配置,与以前相同,唯一的区别是适配器,即现在的Bamboo.TestAdapter。 添加这个`use Bamboo.Test`
可以在我们的测试中使用诸如`assert_delivered_email`
的功能。 现在,在UserController中成功插入后,添加下一行:
Email.create_mail(user.password, user.email)
|> CompanyApi.Mailer.deliver_later
这将创建电子邮件结构并在后台发送它。 对于异步发送,有任务模块。 如果您希望查看已发送的邮件,请在router.exs中添加以下内容:
if Mix.env ==:dev do
forward "/send_mails" , Bamboo.EmailPreviewPlug
end
现在,我们可以在localhost:4000 / sent_mails看到传递的邮件。
到目前为止,我们已经展示了如何编写测试,迁移,模型,控制器,视图和路由。 更重要的一件事是验证用户身份。 这里选择的图书馆是Guardian 。 它使用JWT(Json Web令牌)作为身份验证方法,我们可以对Phoenix服务以及通道进行身份验证。 好东西。
首先在mix.exs文件中添加依赖项`{:guardian, "~> 1.0-beta"}`
并运行
mix deps.get
在Guardian文档中,有详细的说明如何设置基本配置,但我们将在此处逐步进行。 打开/config/config.exs并添加以下内容:
config:company_api , CompanyApi.Guardian,
issuer: "CompanyApi" ,
secret_key: "QDG1lCBdCdjwF49UniOpbxgUINhdyvQDcFQUQam+65O4f9DgWRe09BYMEEDU1i9X" ,
verify_issuer: true
请注意,CompanyApi.Guardian将成为我们要创建的模块。 您不必称其为Guardian,也许有点多余。 无论如何,接下来的事情是必须生成的secret_key。 这是一个秘密密钥的示例,可以通过运行来生成
mix guardian.gen .secret
在lib / company_api /中创建CompanyApi.Guardian模块。
defmodule CompanyApi.Guardian do
use Guardian, otp_app: :company_api
alias CompanyApi.Repo
alias CompanyApiWeb.User
def subject_for_token (user = %User{}, _claims) do
{ :ok , "User: #{user.id} " }
end
def subject_for_token ( _ ) do
{ :error , "Unknown type" }
end
def resource_from_claims (claims) do
id = Enum.at(String.split(claims[ "sub" ], ":" ), 1 )
case Repo.get(User, String.to_integer(id)) do
nil ->
{ :error , "Unknown type" }
user ->
{ :ok , user}
end
end
end
创建令牌时将使用此模块。 我们将用户ID作为令牌的主题,这样我们就可以始终从数据库中获取用户。 这可能是最方便的方法,但不是唯一的方法。 我们要做的下一步是建立守护程序管道。 通过插头使用Guardian很容易。 打开lib / company_api_web / router.ex并添加新管道:
pipeline:auth do
plug Guardian.Plug.Pipeline, module: CompanyApi.Guardian,
error_handler: CompanyApi.GuardianErrorHandler
plug Guardian.Plug.VerifyHeader, realm: "Bearer"
plug Guardian.Plug.EnsureAuthenticated
plug Guardian.Plug.LoadResource, ensure: true
end
该管道可以直接在router.ex文件中定义,也可以在单独的模块中定义,但是仍然需要在此处引用。 当用户尝试调用某些服务时,他的请求将通过管道传递。 请注意,此管道专门用于JSON API 。
Okey,首先,我们定义我们正在使用插件管道和引用实现模块以及将要处理auth错误的模块(我们将创建它)。 下一个插件验证令牌是否在请求标头中,插件确保通过AuthenticAuthenticated确保提供了有效的JWT令牌,最后一个插件通过调用CompanyApi.Guardian模块中指定的函数resource_from_claims / 1来加载资源。
由于缺少auth_error处理模块,请将其添加到lib / company_api /中 。
defmodule CompanyApi.GuardianErrorHandler do
def auth_error (conn, {_type, reason}, _opts) do
conn
|> Plug.Conn.put_resp_content_type( "application/json" )
|> Plug.Conn.send_resp( 401 , Poison.encode!(%{ message: to_string(reason)}))
end
end
毒药是Elixir JSON库。 只需在mix.exs中添加依赖项`{:poison, "~> 3.1"}`
。
我们已经为Guardian设置了所有内容,现在是时候编写SessionController并处理登录和注销了。 首先,我们必须编写测试。 创建session_controller_test.exs。 我们将测试用户登录并使其通过。 我们已经为UserController编写了测试,因此您也知道如何设置这一测试。
test"login as user" , %{ conn: conn, user: user} do
user_credentials = %{ email: user.email, password: user.password}
response =
post(conn, session_path(conn, :create ), creds: user_credentials)
|> json_response( 200 )
expected = %{
"id" => user.id,
"name" => user.name,
"subname" => user.subname,
"password" => user.password,
"email" => user.email,
"job" => user.job
}
assert response[ "data" ][ "user" ] == expected
refute response[ "data" ][ "token" ] == nil
refute response[ "data" ][ "expire" ] == nil
end
我们将尝试使用有效的凭据登录,并期望以响应用户身份获得令牌和过期值。 如果我们运行此测试,它将失败。 我们没有session_path路由。 打开router.ex文件,并在我们的“ / api”范围内添加新路由:
post"/login" , SessionController, :create
我们将此路由置于“ / api”范围内,因为我们的用户在尝试登录时不需要进行身份验证。 如果我们再次运行测试,这次将失败,因为没有创建功能。
现在添加SessionController并编写登录功能。
def create (conn, %{ "creds" => params}) do
new_params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end )
case User.check_registration(new_params) do
{ :ok , user} ->
new_conn = Guardian.Plug.sign_in(conn, CompanyApi.Guardian, user)
token = Guardian.Plug.current_token(new_conn)
claims = Guardian.Plug.current_claims(new_conn)
expire = Map.get(claims, "exp" )
new_conn
|> put_resp_header( "authorization" , "Bearer #{token} " )
|> put_status( :ok )
|> render( "login.json" , user: user, token: token, exp: expire)
{ :error , reason} ->
conn
|> put_status( 401 )
|> render( "error.json" , message: reason)
end
end
第一行以键为原子的结果创建新地图。 函数check_registration / 1检查数据库中是否存在具有给定凭据的用户。 如果用户存在,我们将其登录,创建新令牌并终止日期。 之后,我们设置响应头,状态和渲染用户。 为了渲染,我们需要在lib / company_api_web / views /中创建session_view.ex。
defmodule CompanyApiWeb.SessionView do
use CompanyApiWeb, :view
def render ( "login.json" , %{ user: user, token: token, exp: expire}) do
%{
data: %{
user: render_one(user, CompanyApiWeb.UserView, "user.json" ),
token: token,
expire: expire
}
}
end
def render ( "error.json" , %{ message: reason}) do
%{ data: reason}
end
end
现在测试应该通过了。 当然,应该添加更多测试,但这取决于您。 注销非常简单,`Guardian.revoke(CompanyApi.Guardian,token)`从标头中删除令牌,这就是我们需要做的。 使用API并没有真正的注销,但这是可行的。 在添加新的登出路径之前,我们需要定义“新范围”。 实际上,这将再次成为相同的“ / api”作用域,但是现在它将通过两个管道进行操作: `pipe_through [:api, :auth]`
。
我们为什么这样做呢? 每个需要认证的新路由都将位于此新范围内。 另外,如果要注销,则需要首先进行身份验证。 这样,我们就涵盖了与Guardian进行身份验证的过程。 稍后将提到套接字身份验证,它甚至更容易。
由于这是一个聊天应用程序,因此必须以某种方式保存消息历史记录。 我们将再添加两个实体,它们代表两个用户之间的对话以及用户的消息。 这将是展示Ecto中关联示例的好机会。
我们要添加的第一个实体是对话实体。 对话将同时属于参与聊天的用户,并且该用户将进行许多对话。 对话中还将有许多消息是第二个实体。 消息将属于用户和某些对话。 在这种情况下,用户代表发送消息的人。 消息的其他属性是日期和内容。
我们用几句话描述了我们的数据模型。 这些数据模型中的每一个都有自己的测试,控制器和视图,但是由于我们已经解释了所有这些内容,因此在这一部分中,我们将重点关注这些实体之间的关联。 请注意,您唯一需要做的就是编写用于创建对话,创建消息和获取消息历史记录的功能。
首先,让我们添加对话迁移。
运行命令
mix ecto.gen .migration create_conversations
现在,我们需要使用正确的列创建表对话:
def change do
create table( :conversations ) do
add :sender_id , references( :users , null: false )
add :recipient_id , references( :users , null: false )
timestamps()
end
create unique_index( :conversations , [ :sender_id , :recipient_id ], name: :sender )
end
如您所见,我们正在添加外键sender_id和收件人_id,并且正在引用users表。 这将代表我们两个用户的对话。 两个键不能为null。 我们要做的最后一件事是在与唯一约束相对应的两列上创建unique_index。 我们这样做是因为我们不希望重复的对话具有相同的ID。 现在创建模型:
defmodule CompanyApiWeb.Conversation do
use CompanyApiWeb, :model
alias CompanyApiWeb.{User, Message}
schema "conversations" do
field :status , :string
belongs_to :sender , User, foreign_key: :sender_id
belongs_to :recipient , User, foreign_key: :recipient_id
has_many :messages , Message
timestamps()
end
def changeset (changeset, params \\ %{}) do
changeset
|> cast(params, [ :sender_id , :recipient_id , :status ])
|> validate_required([ :sender_id , :recipient_id ])
|> unique_constraint( :sender_id , name: :sender )
|> foreign_key_constraint( :sender_id )
|> foreign_key_constraint( :recipient_id )
end
观察新功能。 函数belongs_to / 3和has_many / 3表示关联。 通常, belongs_to / 3函数是用名称和引用的模块定义的,但是这一次,因为我们对同一模块有两个引用,所以我们必须添加一个对应的外键列。
has_many / 3关联,关联名称和模块也有同样的情况(我们将很快创建Message模块)。 现在更改集。 我们添加了两个foreign_key_contraint / 3函数,每个外键一个,并且添加了unique_constraint / 3函数(由于复合唯一列,只需要指定一个)。 所有这些约束都在数据库级别检查。
第二实体是消息。 跑
mix ecto.gen .migration create_messages
添加创建和表功能:
def change do
create table( :messages ) do
add :sender_id , references( :users , null: false )
add :conversation_id , references( :conversations , null: false )
add :content , :varchar
add :date , :naive_datetime
timestamps()
end
create index( :messages , [ :sender_id ])
create index( :messages , [ :conversation_id ])
end
和以前一样的故事。 消息属于两个外键,消息分别属于用户(发送者)和会话。 这次我们不需要唯一的约束,所以我们只索引提到的字段。 看一下模型:
defmodule CompanyApiWeb.Message do
use CompanyApiWeb, :model
alias CompanyApiWeb.{User, Conversation}
schema "messages" do
field :content , :string
field :date , :naive_datetime
belongs_to :conversation , Conversation
belongs_to :sender , User, foreign_key: :sender_id
timestamps()
end
def changeset (changeset, params \\ %{}) do
changeset
|> cast(params, [ :sender_id , :conversation_id , :content , :date ])
|> validate_required([ :sender_id , :conversation_id , :content , :date ])
|> foreign_key_constraint( :sender_id )
|> foreign_key_constraint( :conversation_id )
end
我们要做的最后一件事是在“用户”模块中添加关联:
has_many:sender_conversations , Conversation, foreign_key: :sender_id
has_many :recipient_conversations , Conversation, foreign_key: :recipient_id
has_many :messages , Message, foreign_key: :sender_id
这样,我们就建立了数据模型,并且您已经看到了Ecto关联的简要示例。 对于many_to_many关联,请阅读docs 。
本质上,通道是基于套接字顶部的Phoenix抽象。 一个套接字连接上可以有多个通道。 有关详细说明和了解渠道推荐的方式,请阅读官方文档 。
我们的目标是通过Websocket协议发送消息,而我们将从编写通道测试开始。 有关频道测试的文档确实很有帮助。
在/ test / company_api_web / channels /目录中创建chat_room_test.exs。 在设置块中,将一个用户插入数据库,创建连接并登录用户。 我们将测试消息发送。
defmodule CompanyApiWeb.ChatRoomTest do
use CompanyApiWeb.ChannelCase
alias CompanyApi.Guardian, as: Guard
alias CompanyApiWeb.{ChatRoom, UserSocket, Conversation}
@first_user_data %{ name: "John" ,
subname: "Doe" ,
email: "doe@gmail.com" ,
job: "engineer"
}
@second_user_data %{ name: "Jane" ,
subname: "Doe" ,
email: "jane@gmail.com" ,
job: "architect"
}
setup do
user =
%User{}
|> User.reg_changeset( @first_user_data )
|> Repo.insert!
{ :ok , token, _claims} = Guard.encode_and_sign(user)
{ :ok , soc} = connect(UserSocket, %{ "token" => token})
{ :ok , _ , socket} = subscribe_and_join(soc, ChatRoom, "room:chat" )
{ :ok , socket: socket, user: user}
end
test "checks messaging" , %{ socket: socket, user: u} do
user =
%User{}
|> User.reg_changeset( @second_user_data )
|> Repo.insert!
conv =
%Conversation{}
|> Conversation.changeset(%{ sender_id: u.id, recipient_id: user.id})
|> Repo.insert!
{ :ok , token, _claims} = Guard.encode_and_sign(user)
{ :ok , soc} = connect(UserSocket, %{ "token" => token})
{ :ok , _ , socketz} = subscribe_and_join(soc, ChatRoom, "room:chat" )
push socket, "send_msg" , %{ user: user.id, conv: conv.id, message: "Hi! This is message" }
assert_push "receive_msg" , %{ message: message}
assert message.content == "Hi! This is message"
refute Repo.get!(CompanyApiWeb.Message, message.id) == nil
push socketz, "send_msg" , %{ user: u.id, conv: conv.id, message: "This is a reply" }
assert_push "receive_msg" , %{ message: reply}
assert reply.content == "This is a reply"
refute Repo.get!(CompanyApiWeb.Message, reply.id) == nil
end
end
好吧,这似乎很多,但请逐步进行。 在设置块中,我们使用生成的令牌连接到套接字,然后函数subscribe_and_join / 3将用户加入列出的主题。 在测试之后,对第二个用户重复这些步骤,然后创建对话。 函数push / 3允许我们直接通过套接字发送消息,而assert_push或assert_broadcast声明推送或广播的消息。 运行测试将导致错误。
打开lib / company_api_web / channels / user_socket.ex并定义新频道
channel "room:*" , CompanyApiWeb.ChatRoom
在这里修改connect / 2和id / 1函数。 我们希望使只有经过身份验证的用户才能连接到套接字。
def connect (%{ "token" => token}, socket) do
case Guardian.Phoenix.Socket.authenticate(socket, CompanyApi.Guardian, token) do
{ :ok , socket} ->
{ :ok , socket}
{ :error , _ } ->
:error
end
end
def connect (_params, _socket), do: :error
def id (socket) do
user = Guardian.Phoenix.Socket.current_resource(socket)
"user_socket: #{user.id} "
end
`Guardian.Phoenix.Socket.authenticate(socket, CompanyApi.Guardian, token)`
提供了身份验证。 函数id / 1返回套接字ID,我们将其设置为用户ID。
现在让我们创建一个新频道。 在同一目录中创建channel_room.ex文件,但现在保留它。 由于我们正在进行私人聊天,因此我们需要知道要向其发送消息的套接字。 有一些方法可以实现这一目标。 这里的决定是将打开的套接字连接存储在映射`{user_id: socket}`
。
Elixir提供了两种用于存储状态的抽象: GenServers和Agent 。 为了理解GenServer或代理文档的概念,必须阅读。
打开lib / company_api /并创建channel_sessions.ex,这将是我们用于存储套接字的GenServer。
defmodule CompanyApi.ChannelSessions do
use GenServer
#Client side
def start_link (init_state) do
GenServer.start_link(__MODULE_ _ , init_state, name: __MODULE_ _ )
end
def save_socket (user_id, socket) do
GenServer.call(__MODULE_ _ , { :save_socket , user_id, socket})
end
def delete_socket (user_id) do
GenServer.call(__MODULE_ _ , { :delete_socket , user_id})
end
def get_socket (user_id) do
GenServer.call(__MODULE_ _ , { :get_socket , user_id})
end
def clear () do
GenServer.call(__MODULE_ _ , :clear )
end
#Server callbacks
def handle_call ({ :save_socket , user_id, socket}, _from, socket_map) do
case Map.has_key?(socket_map, user_id) do
true ->
{ :reply , socket_map, socket_map}
false ->
new_state = Map.put(socket_map, user_id, socket)
{ :reply , new_state, new_state}
end
end
def handle_call ({ :delete_socket , user_id}, _from, socket_map) do
new_state = Map.delete(socket_map, user_id)
{ :reply , new_state, new_state}
end
def handle_call ({ :get_socket , user_id}, _from, socket_map) do
socket = Map.get(socket_map, user_id)
{ :reply , socket, socket_map}
end
def handle_call ( :clear , _from, state) do
{ :reply , %{}, %{}}
end
end
GenServer抽象了常见的客户端-服务器交互。 客户端调用服务器端回调。 这些回调对地图进行操作。 该模块应在应用程序启动时启动,因此我们将其添加到Supervision tree中 。 这是Elixir中最美丽的东西之一。
在同一目录中打开application.ex文件,并在子级列表中添加`worker(CompanyApi.ChannelSessions, [%{}])`
这一行。 这将以初始状态`%{}`
在应用程序的开头启动ChannelSessions。 现在我们可以编写ChatRoom频道。 每个通道必须实现两个回调join / 3和handle_in / 3 。
defmodule CompanyApiWeb.ChatRoom do
use CompanyApiWeb, :channel
alias CompanyApi.{ChannelSessions, ChannelUsers}
alias CompanyApiWeb.Message
def join ( "room:chat" , _payload, socket) do
user = Guardian.Phoenix.Socket.current_resource(socket)
send( self (), { :after_join , user})
{ :ok , socket}
end
def handle_in ( "send_msg" , %{ "user" => id, "conv" => conv_id, "message" => content}, socket) do
case ChannelSessions.get_socket id do
nil ->
{ :error , socket}
socketz ->
user = Guardian.Phoenix.Socket.current_resource(socket)
case Message.create_message(user.id, conv_id, content) do
nil ->
{ :noreply , socket}
message ->
push socketz, "receive_msg" , %{ message: message}
{ :noreply , socket}
end
end
end
def handle_info ({ :after_join , user}, socket) do
ChannelSessions.save_socket(user.id, socket)
{ :noreply , socket}
end
def terminate (_msg, socket) do
user = Guardian.Phoenix.Socket.current_resource(socket)
ChannelSessions.delete_socket user.id
end
end
由于我们需要保存套接字,因此只能在创建套接字后才能将其完成,该套接字位于join / 3回调的末尾。 因此,我们向自己发送消息,该消息将调用回调方法handle_info / 2 。 在那里,我们将套接字添加到地图中。 回调handle_in / 3创建一条消息并将其发送给适当的用户。 函数teminate / 2从地图上删除套接字。
设置完成后,聊天应用程序API已完成。 本教程涵盖了早期列出的所有部分以及OTP的一些高级内容,例如GenServer。 它旨在在开发一个Elixir应用程序时显示工作流程,并且为了完全理解需要文档阅读。 毕竟,这里有所有信息。
所有Elixir爱好者的推荐场所, Elixir论坛 。
先前发布在https://kolosek.com/elixir-basic-api-guide/
翻译自: https://hackernoon.com/basic-elixir-api-guide-2h48u3y7z
rtems api用户指南