对于 Elixir、Phoenix/Ecto 和 Erlang 来说,总体来说是新手,所以请耐心等待。
我正在使用 Ecto 在 Phoenix 中定义模型、视图和控制器的其他工作示例,但我只是不明白为什么他们的版本可以工作,而我的版本却不能。 我正在尝试使用 TDD 来引导我进入一个工作 API(对于该服务来说是全新的),并且继续遇到这个 Enumerable 问题。
模型(对话)直接取自 Exto 模式和变更集,并使用 Phoenix 的工具将视图和模型链接在一起传递到视图。 虽然此时我没有单独的服务类来隐藏 Ecto 集成,但代码在其他方面似乎几乎与我们系统中其他工作控制器完全相同。那我错过了什么?
这是我的代码段: 路由器代码
scope "/conversations" do
post "/", ConversationController, :create
end
控制器
defmodule ThingyWeb.ConversationController do
use ThingyWeb, :controller
alias Thingy.Conversations.Conversation
alias Thingy.Repo
def create(conn, conversation_params) do
case Thingy.Auth.validate_session(conn) do
{:ok, _} ->
result =
%Conversation{}
|> Conversation.changeset(conversation_params)
|> Repo.insert()
case result do
{:ok, conversation} ->
IO.puts("store to db went well, now trying to render response")
IO.inspect(conversation)
conn
|> put_status(201)
|> render("created.json", conversation)
{:error, _} ->
conn
|> put_status(400)
|> render("error.json", %{message: "Unable to process conversation as provided"})
end
{:error, "No token"} ->
conn
|> put_status(401)
|> render("error.json", %{message: "Authentication failed (no token)"})
{:error, "Not found"} ->
conn
|> put_status(404)
|> render("error.json", %{message: "Authentication failed (not found)"})
end
end
end
型号
defmodule Thingy.Conversations.Conversation do
use Ecto.Schema
import Ecto.Changeset
schema "conversations" do
field :title, :string
field :start_date_time, :utc_datetime
timestamps()
end
@doc false
def changeset(conversation, attrs) do
conversation
|> cast(attrs, [:title, :start_date_time])
|> validate_required([:title, :start_date_time])
end
end
查看
defmodule ThingyWeb.ConversationView do
use ThingyWeb, :view
def render("error.json", %{message: message}) do
%{
errors: [message]
}
end
def render("created.json", %{conversation: conversation}) do
render_one(conversation, ConversationView, "conversation.json")
end
def render("conversation.json", %{conversation: conversation}) do
%{
id: conversation.id,
title: conversation.title
}
end
end
测试
describe "Conversation operations" do
setup [:login_user]
test "able to provide details of a new conversation, and receive the assigned id in response",
%{
conn: conn,
authentication: authentication
} do
IO.puts("starting problem test")
conn =
conn
|> put_req_header("authorization", "Bearer " <> authentication.meallogger_token)
|> put_req_header("content-type", "application/json")
|> post(
Routes.conversation_path(conn, :create),
%{
title: "new conversation",
start_date_time: "2024-06-14T15:30:00Z",
}
)
resp = json_response(conn, 201)
assert %{
"id" => _id,
} = resp
case validate_return_properties(resp, @expected_create_response_properties) do
{:error, extra_keys} ->
assert false, "there were extraneous keys in the json response: #{extra_keys}"
end
end
end
注意:其他测试存在并且不会失败,但它们都是空/错误情况测试,因此不会尝试呈现响应
测试输出和失败
starting problem test
store to db went well, now trying to render response
%Metabite.Conversations.Conversation{
__meta__: #Ecto.Schema.Metadata<:loaded, "conversations">,
id: 50,
title: "new conversation",
start_date_time: ~U[2024-06-14 15:30:00Z],
inserted_at: ~N[2024-06-07 06:30:42],
updated_at: ~N[2024-06-07 06:30:42]
}
Mix task exited with reason
normal
returning code 0
1) test Conversation operations able to provide details of a new conversation, and receive the assigned id in response (ThingyWeb.ConversationControllerTest)
test/thingy_web/controllers/conversation_controller_test.exs:50
** (Protocol.UndefinedError) protocol Enumerable not implemented for %{id: 49, title: "new conversation", __struct__: Thingy.Conversations.Conversation, layout: false, inserted_at: ~N[2024-06-07 06:30:40], conn: %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{id: 49, title: "new conversation", __struct__: Thingy.Conversations.Conversation, layout: false, inserted_at: ~N[2024-06-07 06:30:40], __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, start_date_time: ~U[2024-06-14 15:30:00Z], updated_at: ~N[2024-06-07 06:30:40], current_user: 1}, body_params: %{"start_date_time" => "2024-06-14T15:30:00Z", "title" => "new conversation"}, cookies: %{}, halted: false, host: "www.example.com", method: "POST", owner: #PID<0.587.0>, params: %{"start_date_time" => "2024-06-14T15:30:00Z", "title" => "new conversation"}, path_info: ["api", "v1", "conversations"], path_params: %{}, port: 80, private: %{ThingyWeb.Router => {[], %{PhoenixSwagger.Plug.SwaggerUI => []}}, :phoenix_view => ThingyWeb.ConversationView, :phoenix_template => "created.json", :phoenix_router => ThingyWeb.Router, :phoenix_endpoint => ThingyWeb.Endpoint, :phoenix_action => :create, :phoenix_controller => ThingyWeb.ConversationController, :before_send => [#Function<0.54455629/1 in Plug.Telemetry.call/2>], :plug_session_fetch => #Function<1.76384852/1 in Plug.Session.fetch_session/1>, :plug_skip_csrf_protection => true, :phoenix_recycled => true, :phoenix_request_logger => {"request_logger", "request_logger"}, :phoenix_format => "json", :phoenix_layout => {ThingyWeb.LayoutView, :app}}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{}, req_headers: [{"accept", "application/json"}, {"authorization", "Bearer auth-token-123"}, {"content-type", "application/json"}], request_path: "/api/v1/conversations", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "F9alGnAy2l0PbroAAAbB"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: 201}, __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, start_date_time: ~U[2024-06-14 15:30:00Z], updated_at: ~N[2024-06-07 06:30:40], current_user: 1} of type Thingy.Conversations.Conversation (a struct). This protocol is implemented for the following type(s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Jason.OrderedObject, JasonV.OrderedObject, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, Stream
code: |> post(
stacktrace:
(elixir 1.16.3) lib/enum.ex:1: Enumerable.impl_for!/1
(elixir 1.16.3) lib/enum.ex:166: Enumerable.reduce/3
(elixir 1.16.3) lib/enum.ex:4396: Enum.reverse/1
(elixir 1.16.3) lib/enum.ex:3726: Enum.to_list/1
(elixir 1.16.3) lib/map.ex:224: Map.new_from_enum/1
(phoenix_view 2.0.2) lib/phoenix_view.ex:370: Phoenix.View.render/3
(phoenix_view 2.0.2) lib/phoenix_view.ex:557: Phoenix.View.render_to_iodata/3
(phoenix 1.6.15) lib/phoenix/controller.ex:772: Phoenix.Controller.render_and_send/4
(thingy 0.1.1) lib/thingy_web/controllers/conversation_controller.ex:1: ThingyWeb.ConversationController.action/2
(thingy 0.1.1) lib/thingy_web/controllers/conversation_controller.ex:1: ThingyWeb.ConversationController.phoenix_controller_pipeline/2
(phoenix 1.6.15) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2
(thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint.plug_builder_call/2
(thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint."call (overridable 3)"/2
(thingy 0.1.1) deps/plug/lib/plug/debugger.ex:136: ThingyWeb.Endpoint."call (overridable 4)"/2
(thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint.call/2
(phoenix 1.6.15) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5
test/thingy_web/controllers/conversation_controller_test.exs:60: (test)
好吧,我修好了,但我不知道为什么会这样。
根据@Dogbert的评论,我在控制器代码中进行了以下更改:
conn
|> put_status(201)
|> render("created.json", conversation: conversation)
添加命名可以解决问题,但指出我从我的角度来看缺少内部别名:
defmodule ThingyWeb.ConversationView do
use ThingyWeb, :view
alias ThingyWeb.ConversationView
完成此操作后,根据断言标准,测试按预期失败。
我不确定我是否完全理解为什么这一变化很重要,如果有人可以帮助解释,我将不胜感激。 同样,不确定我是否理解为什么必须将模块别名为自身,以便
render("created.json"
函数能够找到 render("conversation.json"
函数,因此再次感谢任何帮助理解。
无论如何,谢谢Dogbert!