rtems api用户指南_基本的Elixir Api指南

rtems api用户指南

Elixir代表了相对较新的编程语言,面向更广泛的受众。 它于2011年发布,此后一直在开发中。 他的主要特征是取消功能范式,因为它建立在Erlang之上并在BEAM(Erlang VM)上运行。

Elixir专为构建快速,可扩展和可维护的应用程序而设计,而使用Phoenix可以在Web环境中开发这些应用程序。 Phoenix是用Elixir编写的Web框架,它从流行的框架(例如Python的Django或Ruby on Rails)中汲取了很多概念。 如果您熟悉这些,那将是一个不错的起点。

文献资料

Elixir / Phoenix是很好的组合,但是在开始编写应用程序之前,那些不熟悉所有概念的人应该首先阅读以下文档。

  • Elixir-详细文档,从Elixir基本类型到混合和OTP等高级内容,
  • 建议使用Dave Thomas 编写的Elixir
  • ExUnit-用于测试的内置框架,
  • Phoenix -Phoenix框架文档,包括所有概念,并带有示例和
  • Ecto -Elixir的ORM的文档和API。

设置应用

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文件中。

创建API

在编码之前,将解释开发的几个部分:

  • 使用ExUnit编写测试,测试模型和控制器,
  • 编写迁移,
  • 编写模型
  • 编写控制器,
  • 路由,
  • 撰写意见,
  • 使用Guardian和
  • 频道。

请注意,以下部分不会针对整个应用程序进行描述,但是您会明白的。

测试和编写模型

在开发过程中,我们想编写干净的代码,并且还要考虑规范以及代码在实现之前需要做什么。 这就是为什么我们使用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 / 2changeet.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 / 3has_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_pushassert_broadcast声明推送或广播的消息。 运行测试将导致错误。

打开lib / company_api_web / channels / user_socket.ex并定义新频道

channel "room:*" , CompanyApiWeb.ChatRoom

在这里修改connect / 2id / 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提供了两种用于存储状态的抽象: GenServersAgent 。 为了理解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 / 3handle_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用户指南

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值