Skip to content

Commit 5a8db51

Browse files
authored
feat: Address code review / Improved and consistent error formats (#22)
1 parent 17974b7 commit 5a8db51

28 files changed

Lines changed: 247 additions & 119 deletions

.credo.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
# Controversial and experimental checks (opt-in, just move the check to `:enabled`
174174
# and be sure to use `mix credo --strict` to see low priority checks)
175175
#
176+
{Credo.Check.Consistency.ExceptionNames, []},
176177
{Credo.Check.Consistency.MultiAliasImportRequireUse, []},
177178
{Credo.Check.Consistency.UnusedVariableNames, []},
178179
{Credo.Check.Design.DuplicatedCode, []},

.formatter.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Used by "mix format"
22
[
3-
inputs: ["{mix,.formatter,.iex}.exs", "{config,lib,test}/**/*.{ex,exs}"],
3+
inputs: ["{mix,.formatter,.iex,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"],
44
locals_without_parens: [path: 1, method: 1, type: 1, field: 2, field: 3]
55
]

README.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ If you only want to write events in case a subject (such as `/books/42`) does no
7070

7171
```elixir
7272
written = Eventsourcingdb.write_events(
73-
client,
74-
[event],
73+
client,
74+
[event],
7575
[%Eventsourcingdb.IsSubjectPristine{subject: "/books/42"}]
7676
)
7777

@@ -87,8 +87,8 @@ If you only want to write events in case a subject (such as `/books/42`) already
8787

8888
```elixir
8989
written = Eventsourcingdb.write_events(
90-
client,
91-
[event],
90+
client,
91+
[event],
9292
[%Eventsourcingdb.IsSubjectPopulated{subject: "/books/42"}]
9393
)
9494

@@ -104,8 +104,8 @@ If you only want to write events in case the last event of a subject (such as `/
104104

105105
```elixir
106106
written = Eventsourcingdb.write_events(
107-
client,
108-
[event],
107+
client,
108+
[event],
109109
[%Eventsourcingdb.IsSubjectOnEventId{subject: "/books/42", event_id: "0"}]
110110
)
111111

@@ -123,8 +123,8 @@ If you want to write events depending on an EventQL query, use the `IsEventQLQue
123123

124124
```elixir
125125
written = Eventsourcingdb.write_events(
126-
client,
127-
[event],
126+
client,
127+
[event],
128128
[%Eventsourcingdb.IsEventQLQueryTrue{
129129
query: "FROM e IN events WHERE e.type == 'io.eventsourcingdb.library.book-borrowed' PROJECT INTO COUNT () < 10"
130130
}]
@@ -158,8 +158,8 @@ If you want to read not only all the events of a subject, but also the events of
158158

159159
```elixir
160160
result = Eventsourcingdb.read_events(
161-
client,
162-
"/books/42",
161+
client,
162+
"/books/42",
163163
%Eventsourcingdb.ReadEventsOptions{recursive: true}
164164
)
165165
```
@@ -170,8 +170,8 @@ By default, events are read in chronological order. To read in anti-chronologica
170170

171171
```elixir
172172
result = Eventsourcingdb.read_events(
173-
client,
174-
"/books/42",
173+
client,
174+
"/books/42",
175175
%Eventsourcingdb.ReadEventsOptions{
176176
recursive: false,
177177
order: :antichronological
@@ -189,8 +189,8 @@ Specify the ID and whether to include or exclude it, for both the lower and uppe
189189

190190
```elixir
191191
result = Eventsourcingdb.read_events(
192-
client,
193-
"/books/42",
192+
client,
193+
"/books/42",
194194
%Eventsourcingdb.ReadEventsOptions{
195195
recursive: false,
196196
lower_bound: %Eventsourcingdb.BoundOptions{
@@ -213,8 +213,8 @@ Possible options are `:read_nothing`, which skips reading entirely, or `:read_ev
213213

214214
```elixir
215215
result = Eventsourcingdb.read_events(
216-
client,
217-
"/books/42",
216+
client,
217+
"/books/42",
218218
%Eventsourcingdb.ReadEventsOptions{
219219
recursive: false,
220220
from_latest_event: %Eventsourcingdb.FromLatestEventOptions{
@@ -306,7 +306,7 @@ result = Eventsourcingdb.observe_events(
306306
"/books/42",
307307
%Eventsourcingdb.ObserveEventsOptions{
308308
recursive: false,
309-
from_latest_event: %Eventsourcingdb.FromLatestEvevntOptions{
309+
from_latest_event: %Eventsourcingdb.FromLatestEventOptions{
310310
subject: "/books/42",
311311
type: "io.eventsourcingdb.library.book-borrowed",
312312
if_event_is_missing: :read_everything
@@ -413,8 +413,8 @@ By default, `TestContainer` uses the `latest` tag of the official EventSourcingD
413413

414414
```elixir
415415
container(
416-
:esdb,
417-
TestContainer.new()
416+
:esdb,
417+
TestContainer.new()
418418
|> TestContainer.with_image_tag("1.0.0")
419419
)
420420
```
@@ -423,7 +423,7 @@ Similarly, you can configure the port to use and the API token. Call the `with_p
423423

424424
```elixir
425425
container(
426-
:esdb,
426+
:esdb,
427427
TestContainer.new()
428428
|> TestContainer.with_port(4000)
429429
|> TestContainer.with_api_token("secret")

lib/eventsourcingdb.ex

Lines changed: 73 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,30 @@ defmodule Eventsourcingdb do
33
`Eventsourcingdb` client SDK.
44
"""
55

6-
alias Eventsourcingdb.ObserveEventsOptions
7-
alias Eventsourcingdb.ReadEventsOptions
8-
alias Eventsourcingdb.Client
9-
alias Eventsourcingdb.EventType
10-
alias Eventsourcingdb.ManagementEvent
11-
alias Eventsourcingdb.Event
6+
alias Eventsourcingdb.{
7+
ObserveEventsOptions,
8+
ReadEventsOptions,
9+
Client,
10+
Event,
11+
EventCandidate,
12+
EventType,
13+
ManagementEvent
14+
}
15+
16+
alias Eventsourcingdb.{
17+
IsSubjectPristine,
18+
IsSubjectPopulated,
19+
IsSubjectOnEventId,
20+
IsEventQLTrue
21+
}
22+
23+
alias Eventsourcingdb.Errors.{
24+
ApiError,
25+
DBError,
26+
InvalidServerHeader,
27+
InvalidResponseType,
28+
TransmissionError
29+
}
1230

1331
alias Eventsourcingdb.Requests.{
1432
ObserveEvents,
@@ -30,28 +48,44 @@ defmodule Eventsourcingdb do
3048
@typedoc """
3149
The response format for a request
3250
"""
33-
@type response(t) :: {:ok, t} | {:error, any()}
51+
@type primitive_response() :: :ok | {:error, Exception.t()}
52+
53+
@typedoc """
54+
The response format for a request
55+
"""
56+
@type response(t) :: {:ok, t} | {:error, Exception.t()}
57+
58+
@typedoc """
59+
The response format for a force request
60+
"""
61+
@type response!(t) :: t
3462

3563
@typedoc """
3664
The response format for a request returning a stream
3765
"""
38-
@type stream_response(t) :: {:ok, Enumerable.t(t)} | {:error, any()}
66+
@type stream_response(t) :: {:ok, Enumerable.t(t)} | {:error, Exception.t()}
3967

4068
@typedoc """
4169
The response format for a force request returning a stream
4270
"""
4371
@type stream_response!(t) :: Enumerable.t(t)
4472

73+
@type precondition() ::
74+
IsEventQLTrue.t()
75+
| IsSubjectOnEventId.t()
76+
| IsSubjectPopulated.t()
77+
| IsSubjectPristine.t()
78+
4579
@doc """
4680
Pings the DB instance to check if it is reachable.
4781
4882
## Examples
4983
50-
iex> client = %Eventsourcingdb.Client{"http://localhost:3000", "secrettoken"}
84+
iex> client = Eventsourcingdb.Client.new("http://localhost:3000", "secrettoken")
5185
iex> Eventsourcingdb.ping(client)
5286
:ok
5387
"""
54-
@spec ping(Client.t()) :: :ok | {:error, any()}
88+
@spec ping(Client.t()) :: primitive_response()
5589
def ping(client) do
5690
request_one_shot(client, Ping.new())
5791
end
@@ -61,11 +95,11 @@ defmodule Eventsourcingdb do
6195
6296
## Examples
6397
64-
iex> client = %Eventsourcingdb.Client{"http://localhost:3000", "secrettoken"}
98+
iex> client = Eventsourcingdb.Client.new("http://localhost:3000", "secrettoken")
6599
iex> Eventsourcingdb.verify_api_token(client)
66100
:ok
67101
"""
68-
@spec verify_api_token(Client.t()) :: :ok | {:error, any()}
102+
@spec verify_api_token(Client.t()) :: primitive_response()
69103
def verify_api_token(client) do
70104
request_one_shot(client, VerifyApiToken.new())
71105
end
@@ -174,12 +208,14 @@ defmodule Eventsourcingdb do
174208
end
175209
```
176210
"""
177-
@spec write_events(Client.t(), maybe_improper_list(), any()) :: response(Event.t())
211+
@spec write_events(Client.t(), nonempty_list(EventCandidate.t()), [precondition()]) ::
212+
response(Event.t())
178213
def write_events(client, events, preconditions \\ []) when is_list(events) do
179214
request_one_shot(client, WriteEvents.new(events, preconditions))
180215
end
181216

182-
@spec write_events!(Client.t(), maybe_improper_list(), any()) :: Event.t()
217+
@spec write_events!(Client.t(), nonempty_list(EventCandidate.t()), [precondition()]) ::
218+
response!(Event.t())
183219
def write_events!(client, events, preconditions \\ []) when is_list(events) do
184220
request_one_shot!(client, WriteEvents.new(events, preconditions))
185221
end
@@ -297,7 +333,7 @@ defmodule Eventsourcingdb do
297333
The function returns a stream from which you can retrieve one event at a time:
298334
299335
```elixir
300-
result = Eventsourcingdb.observe_events("/books/42")
336+
result = Eventsourcingdb.observe_events(client, "/books/42")
301337
302338
case result do
303339
{:ok, events} -> Enum.to_list(events)
@@ -352,7 +388,7 @@ defmodule Eventsourcingdb do
352388
"/books/42",
353389
%Eventsourcingdb.ObserveEventsOptions{
354390
recursive: false,
355-
from_latest_event: %Eventsourcingdb.FromLatestEvevntOptions{
391+
from_latest_event: %Eventsourcingdb.FromLatestEventOptions{
356392
subject: "/books/42",
357393
type: "io.eventsourcingdb.library.book-borrowed",
358394
if_event_is_missing: :read_everything
@@ -429,7 +465,7 @@ defmodule Eventsourcingdb do
429465
request_one_shot(client, RegisterEventSchema.new(event_type, schema))
430466
end
431467

432-
@spec register_event_schema!(Client.t(), String.t(), map()) :: ManagementEvent.t()
468+
@spec register_event_schema!(Client.t(), String.t(), map()) :: response!(ManagementEvent.t())
433469
def register_event_schema!(client, event_type, schema) do
434470
request_one_shot!(client, RegisterEventSchema.new(event_type, schema))
435471
end
@@ -477,7 +513,7 @@ defmodule Eventsourcingdb do
477513
request_one_shot(client, ReadEventType.new(event_type))
478514
end
479515

480-
@spec read_event_type!(Client.t(), String.t()) :: EventType.t()
516+
@spec read_event_type!(Client.t(), String.t()) :: response!(EventType.t())
481517
def read_event_type!(client, event_type) do
482518
request_one_shot!(client, ReadEventType.new(event_type))
483519
end
@@ -496,16 +532,17 @@ defmodule Eventsourcingdb do
496532
# region Requests
497533
#
498534

499-
@spec request_stream!(Client.t(), struct()) :: any()
535+
@spec request_stream!(Client.t(), struct()) :: stream_response!(any())
500536
defp request_stream!(client, request) do
501537
result = request_stream(client, request)
502538

503539
case result do
504540
{:ok, stream} -> stream
505-
{:error, type, reason} -> raise(type, reason)
541+
{:error, reason} -> raise(reason)
506542
end
507543
end
508544

545+
@spec request_stream(Client.t(), struct()) :: stream_response(any())
509546
defp request_stream(client, request) do
510547
case open_stream(client, request) do
511548
{:ok, response} ->
@@ -524,11 +561,12 @@ defmodule Eventsourcingdb do
524561

525562
{:ok, stream}
526563

527-
{:error, type, reason} ->
528-
{:error, type, reason}
564+
{:error, reason} ->
565+
{:error, reason}
529566
end
530567
end
531568

569+
@spec open_stream(Client.t(), struct()) :: response(any())
532570
defp open_stream(client, request) do
533571
response =
534572
client
@@ -600,25 +638,24 @@ defmodule Eventsourcingdb do
600638
^expected_type ->
601639
{:ok, request_module.process(payload)}
602640

603-
# Forward Errors from the DB as :db_error
641+
# Forward Errors from the DB as %DBError{}
604642
"error" ->
605-
{:error, :db_error, payload}
643+
{:error, %DBError{payload: payload}}
606644

607645
# Ignore heartbeat messages.
608646
"heartbeat" ->
609647
nil
610648

611649
other ->
612-
{:error, :invalid_response_type,
613-
"Expected type \"#{expected_type}\", but got \"#{other}\""}
650+
{:error, %InvalidResponseType{expected: expected_type, actual: other}}
614651
end
615652

616653
{:error, reason} ->
617654
{:error, reason}
618655
end
619656
end
620657

621-
@spec request_one_shot(Client.t(), struct()) :: any()
658+
@spec request_one_shot(Client.t(), struct()) :: response(any())
622659
defp request_one_shot(client, request) do
623660
request_module = get_request_module(request)
624661

@@ -675,7 +712,6 @@ defmodule Eventsourcingdb do
675712
|> Keyword.merge(client.req_options)
676713

677714
Req.new(opts)
678-
# |> Req.Request.append_request_steps(inspect: &IO.inspect/1)
679715
end
680716

681717
defp implements_protocol?(protocol, mod) when is_atom(protocol) and is_struct(mod) do
@@ -702,19 +738,20 @@ defmodule Eventsourcingdb do
702738
#
703739

704740
defp validate_transmission({:error, reason}) do
705-
{:error, :transmission_error, reason}
741+
{:error, %TransmissionError{reason: reason}}
706742
end
707743

708744
defp validate_transmission({:ok, _}), do: {:ok}
709745

710746
@spec validate_server_headers({:ok, Req.Response.t()}) ::
711-
{:ok} | {:error, :invalid_server_header}
747+
{:ok} | {:error, InvalidServerHeader.t()}
712748
defp validate_server_headers({:ok, response}) do
713-
case response
714-
|> Req.Response.get_header("Server")
715-
|> Enum.any?(fn val -> String.starts_with?(val, "EventSourcingDB/") end) do
716-
true -> {:ok}
717-
false -> {:error, :invalid_server_header}
749+
if response
750+
|> Req.Response.get_header("Server")
751+
|> Enum.any?(fn val -> String.starts_with?(val, "EventSourcingDB/") end) do
752+
{:ok}
753+
else
754+
{:error, %InvalidServerHeader{}}
718755
end
719756
end
720757

@@ -723,7 +760,7 @@ defmodule Eventsourcingdb do
723760
end
724761

725762
defp validate_response({:ok, %{body: body}}) do
726-
{:error, :api_error, body}
763+
{:error, %ApiError{reason: body}}
727764
end
728765

729766
defp validate_request_response(response, request_module) do

0 commit comments

Comments
 (0)