From 3726ebd549cbbef9e4bd25d8e52f11d7a12e6664 Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Sun, 25 Jun 2023 19:47:05 -0400 Subject: [PATCH 1/9] Add .tool-versions --- .tool-versions | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..bd4757f --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.15.0-otp-25 +erlang 25.3.2.2 From 1671bab4c7b11bb7147b6a1baab19a1f388519c1 Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Sun, 25 Jun 2023 19:47:22 -0400 Subject: [PATCH 2/9] Upgrade dependencies --- .credo.exs | 274 +++++++++++------- .github/workflows/elixir.yml | 2 + lib/ex_rets.ex | 4 +- lib/ex_rets/http_client/httpc.ex | 18 +- lib/ex_rets/http_client/mock.ex | 4 +- lib/ex_rets/http_middleware/auth_headers.ex | 4 +- .../http_middleware/default_headers.ex | 4 +- lib/ex_rets/http_middleware/logger.ex | 4 +- lib/ex_rets/http_middleware/login.ex | 4 +- lib/ex_rets/http_request.ex | 18 +- lib/ex_rets/http_response.ex | 4 +- lib/ex_rets/rets_client.ex | 18 +- lib/ex_rets/search_arguments.ex | 3 +- mix.exs | 5 +- mix.lock | 26 +- test/ex_rets/http_request_test.exs | 8 +- test/ex_rets/middleware_test.exs | 4 +- 17 files changed, 238 insertions(+), 166 deletions(-) diff --git a/.credo.exs b/.credo.exs index 9637125..c92567a 100644 --- a/.credo.exs +++ b/.credo.exs @@ -10,7 +10,7 @@ configs: [ %{ # - # Run any exec using `mix credo -C `. If no exec name is given + # Run any config using `mix credo -C `. If no config name is given # "default" is used. # name: "default", @@ -21,7 +21,16 @@ # You can give explicit globs or simply directories. # In the latter case `**/*.{ex,exs}` will be used. # - included: ["lib/", "src/", "test/", "web/", "apps/"], + included: [ + "lib/", + "src/", + "test/", + "web/", + "apps/*/lib/", + "apps/*/src/", + "apps/*/test/", + "apps/*/web/" + ], excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] }, # @@ -39,6 +48,10 @@ # strict: true, # + # To modify the timeout for parsing files, change this value: + # + parse_timeout: 5000, + # # If you want to use uncolored output by default, you can change `color` # to `false` below: # @@ -51,113 +64,168 @@ # # {Credo.Check.Design.DuplicatedCode, false} # - checks: [ - # - ## Consistency Checks - # - {Credo.Check.Consistency.ExceptionNames, []}, - {Credo.Check.Consistency.LineEndings, []}, - {Credo.Check.Consistency.ParameterPatternMatching, []}, - {Credo.Check.Consistency.SpaceAroundOperators, []}, - {Credo.Check.Consistency.SpaceInParentheses, []}, - {Credo.Check.Consistency.TabsOrSpaces, []}, + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, - # - ## Design Checks - # - # You can customize the priority of any check - # Priority values are: `low, normal, high, higher` - # - {Credo.Check.Design.AliasUsage, - [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, - # You can also customize the exit_status of each check. - # If you don't want TODO comments to cause `mix credo` to fail, just - # set this value to 0 (zero). - # - {Credo.Check.Design.TagTODO, [exit_status: 0]}, - {Credo.Check.Design.TagFIXME, []}, + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 0]}, + {Credo.Check.Design.TagFIXME, []}, - # - ## Readability Checks - # - {Credo.Check.Readability.AliasOrder, []}, - {Credo.Check.Readability.FunctionNames, []}, - {Credo.Check.Readability.LargeNumbers, []}, - {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 98]}, - {Credo.Check.Readability.ModuleAttributeNames, []}, - {Credo.Check.Readability.ModuleDoc, []}, - {Credo.Check.Readability.ModuleNames, []}, - {Credo.Check.Readability.ParenthesesInCondition, []}, - {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, - {Credo.Check.Readability.PredicateFunctionNames, []}, - {Credo.Check.Readability.PreferImplicitTry, []}, - {Credo.Check.Readability.RedundantBlankLines, []}, - {Credo.Check.Readability.Semicolons, []}, - {Credo.Check.Readability.SpaceAfterCommas, []}, - {Credo.Check.Readability.StringSigils, []}, - {Credo.Check.Readability.TrailingBlankLine, []}, - {Credo.Check.Readability.TrailingWhiteSpace, []}, - {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, - {Credo.Check.Readability.VariableNames, []}, + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 98]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, - # - ## Refactoring Opportunities - # - {Credo.Check.Refactor.CondStatements, []}, - {Credo.Check.Refactor.CyclomaticComplexity, []}, - {Credo.Check.Refactor.FunctionArity, []}, - {Credo.Check.Refactor.LongQuoteBlocks, []}, - {Credo.Check.Refactor.MapInto, false}, - {Credo.Check.Refactor.MatchInCondition, []}, - {Credo.Check.Refactor.NegatedConditionsInUnless, []}, - {Credo.Check.Refactor.NegatedConditionsWithElse, []}, - {Credo.Check.Refactor.Nesting, []}, - {Credo.Check.Refactor.UnlessWithElse, []}, - {Credo.Check.Refactor.WithClauses, []}, + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, - # - ## Warnings - # - {Credo.Check.Warning.BoolOperationOnSameValues, []}, - {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, - {Credo.Check.Warning.IExPry, []}, - {Credo.Check.Warning.IoInspect, []}, - {Credo.Check.Warning.LazyLogging, false}, - {Credo.Check.Warning.OperationOnSameValues, []}, - {Credo.Check.Warning.OperationWithConstantResult, []}, - {Credo.Check.Warning.RaiseInsideRescue, []}, - {Credo.Check.Warning.UnusedEnumOperation, []}, - {Credo.Check.Warning.UnusedFileOperation, []}, - {Credo.Check.Warning.UnusedKeywordOperation, []}, - {Credo.Check.Warning.UnusedListOperation, []}, - {Credo.Check.Warning.UnusedPathOperation, []}, - {Credo.Check.Warning.UnusedRegexOperation, []}, - {Credo.Check.Warning.UnusedStringOperation, []}, - {Credo.Check.Warning.UnusedTupleOperation, []}, + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.WrongTestFileExtension, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.UnsafeExec, []}, - # - # Controversial and experimental checks (opt-in, just replace `false` with `[]`) - # - {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, - {Credo.Check.Consistency.UnusedVariableNames, false}, - {Credo.Check.Design.DuplicatedCode, []}, - {Credo.Check.Readability.AliasAs, []}, - {Credo.Check.Readability.MultiAlias, []}, - {Credo.Check.Readability.Specs, []}, - {Credo.Check.Readability.SinglePipe, []}, - {Credo.Check.Refactor.ABCSize, []}, - {Credo.Check.Refactor.AppendSingleItem, []}, - {Credo.Check.Refactor.DoubleBooleanNegation, []}, - {Credo.Check.Refactor.ModuleDependencies, false}, - {Credo.Check.Refactor.PipeChainStart, []}, - {Credo.Check.Refactor.VariableRebinding, []}, - {Credo.Check.Warning.MapGetUnsafePass, []}, - {Credo.Check.Warning.UnsafeToAtom, []} + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + ], + disabled: [ + # + ## Readability Checks + # + {Credo.Check.Readability.ModuleDoc, []}, - # - # Custom checks can be created using `mix credo.gen.check`. - # - ] + # + ## Warnings + # + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + + # + # Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`) + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Warning.LazyLogging, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } } ] } diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 4478327..422fc8e 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -64,5 +64,7 @@ jobs: run: mix format --check-formatted --dry-run - name: credo run: mix credo --verbose + - name: gradient + run: mix gradient - name: dialyzer run: mix dialyzer --halt-exit-status diff --git a/lib/ex_rets.ex b/lib/ex_rets.ex index 3a7c69c..13f9376 100644 --- a/lib/ex_rets.ex +++ b/lib/ex_rets.ex @@ -4,8 +4,6 @@ defmodule ExRets do """ @moduledoc since: "0.1.0" - require Logger - alias ExRets.CapabilityUris alias ExRets.Credentials alias ExRets.HttpClient.Httpc @@ -19,6 +17,8 @@ defmodule ExRets do alias ExRets.SearchArguments alias ExRets.SearchResponse + require Logger + @typedoc "RETS client." @typedoc since: "0.1.0" @opaque client :: RetsClient.t() diff --git a/lib/ex_rets/http_client/httpc.ex b/lib/ex_rets/http_client/httpc.ex index 0788081..06ba712 100644 --- a/lib/ex_rets/http_client/httpc.ex +++ b/lib/ex_rets/http_client/httpc.ex @@ -2,24 +2,22 @@ defmodule ExRets.HttpClient.Httpc do @moduledoc false @moduledoc since: "0.1.0" + @behaviour ExRets.HttpClient + use GenServer, restart: :transient alias ExRets.HttpClient alias ExRets.HttpRequest alias ExRets.HttpResponse - @behaviour HttpClient - - @default_http_opts [ssl: [ciphers: :ssl.cipher_suites(:all, :"tlsv1.2")]] - @typedoc since: "0.1.0" - @type url :: charlist() + @type url :: String.t() @typedoc since: "0.1.0" - @type field :: charlist() + @type field :: [byte()] @typedoc since: "0.1.0" - @type value :: charlist() + @type value :: String.t() @typedoc since: "0.1.0" @type header :: {field(), value()} @@ -28,10 +26,10 @@ defmodule ExRets.HttpClient.Httpc do @type headers :: [header()] @typedoc since: "0.1.0" - @type content_type :: charlist() + @type content_type :: String.t() @typedoc since: "0.1.0" - @type body :: String.t() | charlist() + @type body :: String.t() @typedoc since: "0.1.0" @type request :: {url(), headers()} | {url(), headers(), content_type(), body()} @@ -185,7 +183,7 @@ defmodule ExRets.HttpClient.Httpc do {:http, {request_id, :stream_start, headers, httpc_stream_pid}}, %{from: from, request_id: request_id} = state ) do - response = HttpResponse.from_httpc({{'HTTP/1.1', 200, 'OK'}, headers, ''}) + response = HttpResponse.from_httpc({{'HTTP/1.1', 200, 'OK'}, headers, ""}) GenServer.reply(from, {:ok, response}) {:noreply, %{state | from: nil, httpc_stream_pid: httpc_stream_pid}} end diff --git a/lib/ex_rets/http_client/mock.ex b/lib/ex_rets/http_client/mock.ex index c9e9297..d4428d3 100644 --- a/lib/ex_rets/http_client/mock.ex +++ b/lib/ex_rets/http_client/mock.ex @@ -2,12 +2,12 @@ defmodule ExRets.HttpClient.Mock do @moduledoc false @moduledoc since: "0.1.0" + @behaviour ExRets.HttpClient + alias ExRets.HttpClient alias ExRets.HttpRequest alias ExRets.HttpResponse - @behaviour HttpClient - defstruct [:name, :response, :stream] @impl HttpClient diff --git a/lib/ex_rets/http_middleware/auth_headers.ex b/lib/ex_rets/http_middleware/auth_headers.ex index beef3f0..9fb7a37 100644 --- a/lib/ex_rets/http_middleware/auth_headers.ex +++ b/lib/ex_rets/http_middleware/auth_headers.ex @@ -2,14 +2,14 @@ defmodule ExRets.Middleware.AuthHeaders do @moduledoc false @moduledoc since: "0.1.0" + @behaviour ExRets.Middleware + alias ExRets.Credentials alias ExRets.HttpAuthentication alias ExRets.HttpRequest alias ExRets.HttpResponse alias ExRets.Middleware - @behaviour Middleware - @impl Middleware @doc since: "0.1.0" def init(%Credentials{} = credentials), do: credentials diff --git a/lib/ex_rets/http_middleware/default_headers.ex b/lib/ex_rets/http_middleware/default_headers.ex index 8f01c31..084a3af 100644 --- a/lib/ex_rets/http_middleware/default_headers.ex +++ b/lib/ex_rets/http_middleware/default_headers.ex @@ -2,12 +2,12 @@ defmodule ExRets.Middleware.DefaultHeaders do @moduledoc false @moduledoc since: "0.1.0" + @behaviour ExRets.Middleware + alias ExRets.Credentials alias ExRets.HttpRequest alias ExRets.Middleware - @behaviour Middleware - @project_version Keyword.fetch!(Mix.Project.config(), :version) @default_user_agent "ExRets/#{@project_version}" diff --git a/lib/ex_rets/http_middleware/logger.ex b/lib/ex_rets/http_middleware/logger.ex index 3f20342..712f509 100644 --- a/lib/ex_rets/http_middleware/logger.ex +++ b/lib/ex_rets/http_middleware/logger.ex @@ -2,12 +2,12 @@ defmodule ExRets.Middleware.Logger do @moduledoc false @moduledoc since: "0.1.0" - require Logger + @behaviour ExRets.Middleware alias ExRets.HttpRequest alias ExRets.Middleware - @behaviour Middleware + require Logger @impl Middleware @doc since: "0.1.0" diff --git a/lib/ex_rets/http_middleware/login.ex b/lib/ex_rets/http_middleware/login.ex index bb68763..5dcbc7d 100644 --- a/lib/ex_rets/http_middleware/login.ex +++ b/lib/ex_rets/http_middleware/login.ex @@ -2,12 +2,12 @@ defmodule ExRets.Middleware.Login do @moduledoc false @moduledoc since: "0.1.0" + @behaviour ExRets.Middleware + alias ExRets.Credentials alias ExRets.HttpRequest alias ExRets.Middleware - @behaviour Middleware - @impl Middleware @doc since: "0.1.0" def init(%Credentials{} = credentials), do: credentials diff --git a/lib/ex_rets/http_request.ex b/lib/ex_rets/http_request.ex index 6d11d35..a9f7641 100644 --- a/lib/ex_rets/http_request.ex +++ b/lib/ex_rets/http_request.ex @@ -11,23 +11,21 @@ defmodule ExRets.HttpRequest do method: :get | :post, uri: URI.t(), headers: ExRets.HttpClient.headers(), - body: String.t() + body: String.t() | nil } @doc false @spec to_httpc(t()) :: Httpc.request() def to_httpc(%__MODULE__{} = request) do - uri = request.uri |> to_string() |> to_charlist() + uri = to_string(request.uri) headers = Enum.map(request.headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end) + body = request.body - case request.method do - :get -> - {uri, headers} - - :post -> - {content_type, headers} = split_content_type_from_headers(headers) - body = to_charlist(request.body) - {uri, headers, content_type, body} + if request.method == :post and is_binary(body) do + {content_type, headers} = split_content_type_from_headers(headers) + {uri, headers, content_type, body} + else + {uri, headers} end end diff --git a/lib/ex_rets/http_response.ex b/lib/ex_rets/http_response.ex index 4328c0f..46d99c0 100644 --- a/lib/ex_rets/http_response.ex +++ b/lib/ex_rets/http_response.ex @@ -27,9 +27,9 @@ defmodule ExRets.HttpResponse do @doc false @doc since: "0.1.0" @spec from_httpc(Httpc.result()) :: t() - def from_httpc({{_, status, _}, headers, body}) do + def from_httpc({{_, status, _}, headers, body}) when is_integer(status) and status > 0 do headers = Enum.map(headers, fn {key, value} -> {to_string(key), to_string(value)} end) - body = IO.iodata_to_binary(body) + body = IO.chardata_to_string(body) %__MODULE__{ status: status, diff --git a/lib/ex_rets/rets_client.ex b/lib/ex_rets/rets_client.ex index f7b02e8..bb05380 100644 --- a/lib/ex_rets/rets_client.ex +++ b/lib/ex_rets/rets_client.ex @@ -4,18 +4,20 @@ defmodule ExRets.RetsClient do alias ExRets.Credentials alias ExRets.HttpClient + alias ExRets.HttpClient.Httpc + alias ExRets.HttpClient.Mock alias ExRets.LoginResponse alias ExRets.Middleware @typedoc since: "0.1.0" - @opaque t :: %__MODULE__{ - credentials: Credentials.t(), - http_client: HttpClient.client(), - http_client_implementation: Httpc | Mock, - http_timeout: non_neg_integer() | :infinity, - login_response: LoginResponse.t(), - middleware: [Middleware.t()] - } + @type t :: %__MODULE__{ + credentials: Credentials.t(), + http_client: HttpClient.client(), + http_client_implementation: Httpc | Mock, + http_timeout: non_neg_integer() | :infinity, + login_response: LoginResponse.t(), + middleware: [Middleware.t()] + } @derive {Inspect, only: [:credentials]} defstruct [ diff --git a/lib/ex_rets/search_arguments.ex b/lib/ex_rets/search_arguments.ex index a34b22e..415b0be 100644 --- a/lib/ex_rets/search_arguments.ex +++ b/lib/ex_rets/search_arguments.ex @@ -160,7 +160,6 @@ defmodule ExRets.SearchArguments do atom |> to_string() |> String.split("_") - |> Enum.map(&String.capitalize/1) - |> Enum.join() + |> Enum.map_join(&String.capitalize/1) end end diff --git a/mix.exs b/mix.exs index ea67f4b..89a5e43 100644 --- a/mix.exs +++ b/mix.exs @@ -25,9 +25,10 @@ defmodule ExRets.MixProject do defp deps do [ + {:credo, "~> 1.1", only: :dev, runtime: false}, + {:dialyxir, "~> 1.3", only: :dev, runtime: false}, {:ex_doc, "~> 0.21", only: :dev, runtime: false}, - {:dialyxir, "~> 1.0.0-rc.7", only: :dev, runtime: false}, - {:credo, "~> 1.1", only: :dev, runtime: false} + {:gradient, github: "esl/gradient", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index 2dbc647..983555a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,18 @@ %{ - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "1.0.0-rc.7", "6287f8f2cb45df8584317a4be1075b8c9b8a69de8eeb82b4d9e6c761cf2664cd", [:mix], [{:erlex, ">= 0.2.5", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, - "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, - "erlex": {:hex, :erlex, "0.2.5", "e51132f2f472e13d606d808f0574508eeea2030d487fc002b46ad97e738b0510", [:mix], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, + "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm", "5e8806285d8a3a8999bd38e4a73c58d28534c856bc38c44818e5ba85bbda16fb"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "gradient": {:git, "https://github.com/esl/gradient.git", "081bbae686e809d4cf1a96ff45c57389a6490ecf", []}, + "gradient_macros": {:git, "https://github.com/esl/gradient_macros.git", "3bce2146bf0cdf380f773c40e2b7bd6558ab6de8", [ref: "3bce214"]}, + "gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "1498d1792155010950c86dc3e92ccb111b706e80", [ref: "1498d17"]}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, } diff --git a/test/ex_rets/http_request_test.exs b/test/ex_rets/http_request_test.exs index ded22c0..61edd61 100644 --- a/test/ex_rets/http_request_test.exs +++ b/test/ex_rets/http_request_test.exs @@ -14,7 +14,7 @@ defmodule ExRets.HttpRequestTest do headers: [{"authorization", "letmein=please"}] } - assert {'https://example.com/login', [{'authorization', 'letmein=please'}]} = + assert {"https://example.com/login", [{'authorization', 'letmein=please'}]} = HttpRequest.to_httpc(request) end @@ -27,8 +27,8 @@ defmodule ExRets.HttpRequestTest do body: ~s/{"letmein":"please"}/ } - assert {'https://example.com/login', [{'authorization', 'letmein=please'}], - 'application/json', '{"letmein":"please"}'} = HttpRequest.to_httpc(request) + assert {"https://example.com/login", [{'authorization', 'letmein=please'}], + 'application/json', ~s/{"letmein":"please"}/} = HttpRequest.to_httpc(request) end @tag :unit @@ -39,7 +39,7 @@ defmodule ExRets.HttpRequestTest do body: "pretty please" } - assert {'https://example.com/login', [], 'text/plain', 'pretty please'} = + assert {"https://example.com/login", [], 'text/plain', "pretty please"} = HttpRequest.to_httpc(request) end end diff --git a/test/ex_rets/middleware_test.exs b/test/ex_rets/middleware_test.exs index 0742e41..9c38a53 100644 --- a/test/ex_rets/middleware_test.exs +++ b/test/ex_rets/middleware_test.exs @@ -30,7 +30,7 @@ defmodule ExRets.MiddlewareTest do @request %HttpRequest{uri: @login_uri} - defmodule __MODULE__.MockMiddleware do + defmodule MockMiddleware do @behaviour Middleware @impl Middleware @@ -48,8 +48,6 @@ defmodule ExRets.MiddlewareTest do end end - alias __MODULE__.MockMiddleware - describe "open_stream/2" do test "calls middleware" do rets_client = %RetsClient{new_rets_client() | middleware: [MockMiddleware]} From 861daf8a6195cb3a216c0aa2acd5c88c18cbb612 Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Sun, 25 Jun 2023 19:48:34 -0400 Subject: [PATCH 3/9] Update and harden httpc ssl opts --- lib/ex_rets/http_client/httpc.ex | 59 +++++++++++++++++++++++++++++++- test/test_helper.exs | 2 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/ex_rets/http_client/httpc.ex b/lib/ex_rets/http_client/httpc.ex index 06ba712..9e65eaf 100644 --- a/lib/ex_rets/http_client/httpc.ex +++ b/lib/ex_rets/http_client/httpc.ex @@ -221,7 +221,64 @@ defmodule ExRets.HttpClient.Httpc do end defp merge_default_http_opts(http_opts) do - Keyword.merge(@default_http_opts, http_opts) + preferred_ciphers = ssl_preferred_ciphers() + ciphers = :ssl.filter_cipher_suites(preferred_ciphers, []) + versions = ssl_versions() + preferred_eccs = ssl_preferred_eccs() + eccs = :ssl.eccs() -- :ssl.eccs() -- preferred_eccs + + default_http_opts = [ + ssl: [ + verify: :verify_peer, + cacerts: :public_key.cacerts_get(), + depth: 3, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ], + ciphers: ciphers, + versions: versions, + eccs: eccs + ] + ] + + Keyword.merge(default_http_opts, http_opts) + end + + defp ssl_preferred_ciphers do + :ex_rets + |> Application.get_env(__MODULE__, []) + |> Keyword.get(:ssl_preferred_ciphers, [ + # Cipher suites (TLS 1.3): TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384: + # TLS_CHACHA20_POLY1305_SHA256 + %{cipher: :aes_128_gcm, key_exchange: :any, mac: :aead, prf: :sha256}, + %{cipher: :aes_256_gcm, key_exchange: :any, mac: :aead, prf: :sha384}, + %{cipher: :chacha20_poly1305, key_exchange: :any, mac: :aead, prf: :sha256}, + # Cipher suites (TLS 1.2): ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256: + # ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305: + # ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 + %{cipher: :aes_128_gcm, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256}, + %{cipher: :aes_128_gcm, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256}, + %{cipher: :aes_256_gcm, key_exchange: :ecdh_ecdsa, mac: :aead, prf: :sha384}, + %{cipher: :aes_256_gcm, key_exchange: :ecdh_rsa, mac: :aead, prf: :sha384}, + %{cipher: :chacha20_poly1305, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256}, + %{cipher: :chacha20_poly1305, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256}, + %{cipher: :aes_128_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha256}, + %{cipher: :aes_256_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha384} + ]) + end + + defp ssl_versions do + :ex_rets + |> Application.get_env(__MODULE__, []) + # Protocols: TLS 1.2, TLS 1.3 + |> Keyword.get(:ssl_versions, [:"tlsv1.2", :"tlsv1.3"]) + end + + defp ssl_preferred_eccs do + :ex_rets + |> Application.get_env(__MODULE__, []) + # TLS curves: X25519, prime256v1, secp384r1 + |> Keyword.get(:ssl_preferred_eccs, [:secp256r1, :secp384r1]) end defp httpc_opts do diff --git a/test/test_helper.exs b/test/test_helper.exs index a321dd8..9f16f68 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,2 +1,2 @@ -ExUnit.start() +ExUnit.start(capture_log: true) ExUnit.configure(exclude: [:reso_dot_org]) From c35b42181546114e61caa549fbb21626535cb89b Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Sun, 25 Jun 2023 22:45:55 -0400 Subject: [PATCH 4/9] Format charlists --- .../digest_access_authentication/response.ex | 8 ++------ lib/ex_rets/http_client/httpc.ex | 2 +- lib/ex_rets/http_request.ex | 4 ++-- lib/ex_rets/login_response.ex | 4 ++-- lib/ex_rets/logout_response.ex | 4 ++-- lib/ex_rets/rets_response.ex | 4 ++-- lib/ex_rets/search_response.ex | 16 ++++++++-------- test/ex_rets/http_request_test.exs | 8 ++++---- test/ex_rets/http_response_test.exs | 3 ++- test/ex_rets/rets_response_test.exs | 4 ++-- 10 files changed, 27 insertions(+), 30 deletions(-) diff --git a/lib/ex_rets/digest_access_authentication/response.ex b/lib/ex_rets/digest_access_authentication/response.ex index f29e0a0..2e4f214 100644 --- a/lib/ex_rets/digest_access_authentication/response.ex +++ b/lib/ex_rets/digest_access_authentication/response.ex @@ -159,9 +159,7 @@ defmodule ExRets.DigestAccessAuthentication.Response do | qop: :auth, response: md5_then_hex( - "#{ha1}:#{response.nonce}:#{format_nonce_count(response.nc)}:#{response.cnonce}:#{ - qop_value_to_string(:auth) - }:#{ha2}" + "#{ha1}:#{response.nonce}:#{format_nonce_count(response.nc)}:#{response.cnonce}:#{qop_value_to_string(:auth)}:#{ha2}" ) } @@ -171,9 +169,7 @@ defmodule ExRets.DigestAccessAuthentication.Response do | qop: :auth_int, response: md5_then_hex( - "#{ha1}:#{response.nonce}:#{format_nonce_count(response.nc)}:#{response.cnonce}:#{ - qop_value_to_string(:auth_int) - }:#{ha2}" + "#{ha1}:#{response.nonce}:#{format_nonce_count(response.nc)}:#{response.cnonce}:#{qop_value_to_string(:auth_int)}:#{ha2}" ) } diff --git a/lib/ex_rets/http_client/httpc.ex b/lib/ex_rets/http_client/httpc.ex index 9e65eaf..5e74af0 100644 --- a/lib/ex_rets/http_client/httpc.ex +++ b/lib/ex_rets/http_client/httpc.ex @@ -183,7 +183,7 @@ defmodule ExRets.HttpClient.Httpc do {:http, {request_id, :stream_start, headers, httpc_stream_pid}}, %{from: from, request_id: request_id} = state ) do - response = HttpResponse.from_httpc({{'HTTP/1.1', 200, 'OK'}, headers, ""}) + response = HttpResponse.from_httpc({{~c"HTTP/1.1", 200, ~c"OK"}, headers, ""}) GenServer.reply(from, {:ok, response}) {:noreply, %{state | from: nil, httpc_stream_pid: httpc_stream_pid}} end diff --git a/lib/ex_rets/http_request.ex b/lib/ex_rets/http_request.ex index a9f7641..a686dac 100644 --- a/lib/ex_rets/http_request.ex +++ b/lib/ex_rets/http_request.ex @@ -30,8 +30,8 @@ defmodule ExRets.HttpRequest do end defp split_content_type_from_headers(headers) do - case List.keytake(headers, 'content-type', 0) do - nil -> {'text/plain', headers} + case List.keytake(headers, ~c"content-type", 0) do + nil -> {~c"text/plain", headers} {{_, ct}, headers} -> {ct, headers} end end diff --git a/lib/ex_rets/login_response.ex b/lib/ex_rets/login_response.ex index 6e70816..6396885 100644 --- a/lib/ex_rets/login_response.ex +++ b/lib/ex_rets/login_response.ex @@ -43,7 +43,7 @@ defmodule ExRets.LoginResponse do end end - defp event_fun({:startElement, _, 'RETS', _, attributes}, _, state) do + defp event_fun({:startElement, _, ~c"RETS", _, attributes}, _, state) do updated_rets_response = RetsResponse.read_rets_element_attributes(attributes, state.rets_response) @@ -58,7 +58,7 @@ defmodule ExRets.LoginResponse do put_in(state.characters, [characters | state.characters]) end - defp event_fun({:endElement, _, 'RETS-RESPONSE', _}, _, state) do + defp event_fun({:endElement, _, ~c"RETS-RESPONSE", _}, _, state) do key_value_body = state.characters |> Enum.reverse() |> Enum.join("") login_response = %__MODULE__{ diff --git a/lib/ex_rets/logout_response.ex b/lib/ex_rets/logout_response.ex index 9b176a9..d096f65 100644 --- a/lib/ex_rets/logout_response.ex +++ b/lib/ex_rets/logout_response.ex @@ -28,7 +28,7 @@ defmodule ExRets.LogoutResponse do end end - defp event_fun({:startElement, _, 'RETS', _, attributes}, _, state) do + defp event_fun({:startElement, _, ~c"RETS", _, attributes}, _, state) do updated_rets_response = RetsResponse.read_rets_element_attributes(attributes, state.rets_response) @@ -43,7 +43,7 @@ defmodule ExRets.LogoutResponse do put_in(state.characters, [characters | state.characters]) end - defp event_fun({:endElement, _, 'RETS-RESPONSE', _}, _, state) do + defp event_fun({:endElement, _, ~c"RETS-RESPONSE", _}, _, state) do key_value_body = state.characters |> Enum.reverse() diff --git a/lib/ex_rets/rets_response.ex b/lib/ex_rets/rets_response.ex index b2ef337..9c59b9c 100644 --- a/lib/ex_rets/rets_response.ex +++ b/lib/ex_rets/rets_response.ex @@ -50,11 +50,11 @@ defmodule ExRets.RetsResponse do @spec read_rets_element_attributes(BaseXmlParser.attributes(), t()) :: t() def read_rets_element_attributes(attributes, %__MODULE__{} = rets_response) do Enum.reduce(attributes, rets_response, fn - {_, _, 'ReplyCode', value}, acc -> + {_, _, ~c"ReplyCode", value}, acc -> reply_code = value |> to_string() |> String.to_integer() put_in(acc.reply_code, reply_code) - {_, _, 'ReplyText', value}, acc -> + {_, _, ~c"ReplyText", value}, acc -> reply_text = to_string(value) put_in(acc.reply_text, reply_text) diff --git a/lib/ex_rets/search_response.ex b/lib/ex_rets/search_response.ex index 2d47ad4..22e827c 100644 --- a/lib/ex_rets/search_response.ex +++ b/lib/ex_rets/search_response.ex @@ -47,16 +47,16 @@ defmodule ExRets.SearchResponse do end end - defp event_fun({:startElement, _, 'RETS', _, attributes}, _, state) do + defp event_fun({:startElement, _, ~c"RETS", _, attributes}, _, state) do updated_rets_response = RetsResponse.read_rets_element_attributes(attributes, state.rets_response) %{state | rets_response: updated_rets_response} end - defp event_fun({:startElement, _, 'COUNT', _, attributes}, _, state) do + defp event_fun({:startElement, _, ~c"COUNT", _, attributes}, _, state) do Enum.reduce(attributes, state, fn - {_, _, 'Records', value}, acc -> + {_, _, ~c"Records", value}, acc -> count = value |> to_string() |> String.to_integer() put_in(acc.rets_response.response.count, count) @@ -65,9 +65,9 @@ defmodule ExRets.SearchResponse do end) end - defp event_fun({:startElement, _, 'DELIMITER', _, attributes}, _, state) do + defp event_fun({:startElement, _, ~c"DELIMITER", _, attributes}, _, state) do Enum.reduce(attributes, state, fn - {_, _, 'value', value}, acc -> + {_, _, ~c"value", value}, acc -> value = to_string(value) case CompactDelimiter.decode(value) do @@ -80,7 +80,7 @@ defmodule ExRets.SearchResponse do end) end - defp event_fun({:startElement, _, 'MAXROWS', _, _attributes}, _, state) do + defp event_fun({:startElement, _, ~c"MAXROWS", _, _attributes}, _, state) do put_in(state.rets_response.response.max_rows, true) end @@ -92,7 +92,7 @@ defmodule ExRets.SearchResponse do put_in(state.characters, [characters | state.characters]) end - defp event_fun({:endElement, _, 'COLUMNS', _}, _, state) do + defp event_fun({:endElement, _, ~c"COLUMNS", _}, _, state) do columns = state.characters |> Enum.reverse() @@ -102,7 +102,7 @@ defmodule ExRets.SearchResponse do put_in(state.rets_response.response.columns, columns) end - defp event_fun({:endElement, _, 'DATA', _}, _, state) do + defp event_fun({:endElement, _, ~c"DATA", _}, _, state) do row = state.characters |> Enum.reverse() diff --git a/test/ex_rets/http_request_test.exs b/test/ex_rets/http_request_test.exs index 61edd61..faecf93 100644 --- a/test/ex_rets/http_request_test.exs +++ b/test/ex_rets/http_request_test.exs @@ -14,7 +14,7 @@ defmodule ExRets.HttpRequestTest do headers: [{"authorization", "letmein=please"}] } - assert {"https://example.com/login", [{'authorization', 'letmein=please'}]} = + assert {"https://example.com/login", [{~c"authorization", ~c"letmein=please"}]} = HttpRequest.to_httpc(request) end @@ -27,8 +27,8 @@ defmodule ExRets.HttpRequestTest do body: ~s/{"letmein":"please"}/ } - assert {"https://example.com/login", [{'authorization', 'letmein=please'}], - 'application/json', ~s/{"letmein":"please"}/} = HttpRequest.to_httpc(request) + assert {"https://example.com/login", [{~c"authorization", ~c"letmein=please"}], + ~c"application/json", ~s/{"letmein":"please"}/} = HttpRequest.to_httpc(request) end @tag :unit @@ -39,7 +39,7 @@ defmodule ExRets.HttpRequestTest do body: "pretty please" } - assert {"https://example.com/login", [], 'text/plain', "pretty please"} = + assert {"https://example.com/login", [], ~c"text/plain", "pretty please"} = HttpRequest.to_httpc(request) end end diff --git a/test/ex_rets/http_response_test.exs b/test/ex_rets/http_response_test.exs index 45b20bc..09cb0aa 100644 --- a/test/ex_rets/http_response_test.exs +++ b/test/ex_rets/http_response_test.exs @@ -8,7 +8,8 @@ defmodule ExRets.HttpResponseTest do describe "from_httpc/1" do test "dumps an :httpc response" do httpc_response = - {{'HTTP/1.1', 200, 'OK'}, [{'cache-control', 'private, max-age=0'}], 'Hello, World!'} + {{~c"HTTP/1.1", 200, ~c"OK"}, [{~c"cache-control", ~c"private, max-age=0"}], + ~c"Hello, World!"} assert %HttpResponse{ status: 200, diff --git a/test/ex_rets/rets_response_test.exs b/test/ex_rets/rets_response_test.exs index 6daa8ab..5a6bb8a 100644 --- a/test/ex_rets/rets_response_test.exs +++ b/test/ex_rets/rets_response_test.exs @@ -6,8 +6,8 @@ defmodule ExRets.RetsResponseTest do doctest RetsResponse @attributes [ - {[], [], 'ReplyCode', '0'}, - {[], [], 'ReplyText', 'Operation Successful'} + {[], [], ~c"ReplyCode", ~c"0"}, + {[], [], ~c"ReplyText", ~c"Operation Successful"} ] describe "read_rets_element_attributes/2" do From 259ab23595e1457003b3b9e1441bd36b5493e200 Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Sun, 25 Jun 2023 23:37:05 -0400 Subject: [PATCH 5/9] Remove use of Task module in ExRets --- .gradient_ignore.exs | 4 ++ lib/ex_rets.ex | 72 +++++++++++----------------------- lib/ex_rets/middleware.ex | 1 - lib/ex_rets/search_response.ex | 4 +- 4 files changed, 29 insertions(+), 52 deletions(-) create mode 100644 .gradient_ignore.exs diff --git a/.gradient_ignore.exs b/.gradient_ignore.exs new file mode 100644 index 0000000..46ccb18 --- /dev/null +++ b/.gradient_ignore.exs @@ -0,0 +1,4 @@ +[ + "lib/ex_rets.ex:100", + "lib/ex_rets.ex:121" +] diff --git a/lib/ex_rets.ex b/lib/ex_rets.ex index 13f9376..3b8c801 100644 --- a/lib/ex_rets.ex +++ b/lib/ex_rets.ex @@ -48,10 +48,7 @@ defmodule ExRets do {:ok, client()} | {:ok, HttpResponse.t()} | {:error, reason()} def login(%Credentials{} = credentials, opts \\ []) do with {:ok, rets_client} <- new_rets_client(credentials, opts) do - rets_client - |> login_fun() - |> Task.async() - |> Task.await(:infinity) + login_client(rets_client) end end @@ -79,17 +76,15 @@ defmodule ExRets do end end - defp login_fun(%RetsClient{} = rets_client) do - fn -> - login_uri = rets_client.credentials.login_uri - request = %HttpRequest{uri: login_uri} - http_client_implementation = rets_client.http_client_implementation - - with {:ok, _response, stream} <- Middleware.open_stream(rets_client, request), - {:ok, rets_response} <- - LoginResponse.parse(stream, login_uri, http_client_implementation) do - {:ok, %RetsClient{rets_client | login_response: rets_response.response}} - end + defp login_client(%RetsClient{} = rets_client) do + login_uri = rets_client.credentials.login_uri + request = %HttpRequest{uri: login_uri} + http_client_implementation = rets_client.http_client_implementation + + with {:ok, _response, stream} <- Middleware.open_stream(rets_client, request), + {:ok, rets_response} <- + LoginResponse.parse(stream, login_uri, http_client_implementation) do + {:ok, %RetsClient{rets_client | login_response: rets_response.response}} end end @@ -102,26 +97,15 @@ defmodule ExRets do An `:error` tuple is returned for any other issues. """ @doc since: "0.1.0" - @dialyzer {:no_contracts, logout: 1} - @spec logout(client()) :: - {:ok, RetsResponse.t()} | {:ok, HttpResponse.t()} | {:error, reason()} + @spec logout(client()) :: {:ok, RetsResponse.t()} | {:ok, HttpResponse.t()} | {:error, reason()} def logout(%RetsClient{} = rets_client) do - rets_client - |> logout_fun() - |> Task.async() - |> Task.await(:infinity) - end + logout_uri = rets_client.login_response.capability_uris.logout + request = %HttpRequest{uri: logout_uri} + http_client_implementation = rets_client.http_client_implementation - defp logout_fun(%RetsClient{} = rets_client) do - fn -> - logout_uri = rets_client.login_response.capability_uris.logout - request = %HttpRequest{uri: logout_uri} - http_client_implementation = rets_client.http_client_implementation - - with {:ok, _response, stream} <- Middleware.open_stream(rets_client, request), - :ok <- http_client_implementation.stop_client(rets_client.http_client) do - LogoutResponse.parse(stream, http_client_implementation) - end + with {:ok, _response, stream} <- Middleware.open_stream(rets_client, request), + :ok <- http_client_implementation.stop_client(rets_client.http_client) do + LogoutResponse.parse(stream, http_client_implementation) end end @@ -134,7 +118,6 @@ defmodule ExRets do An `:error` tuple is returned for any other issues. """ @doc since: "0.1.0" - @dialyzer {:no_contracts, search: 2} @spec search(client(), SearchArguments.t()) :: {:ok, RetsResponse.t()} | {:ok, HttpResponse.t()} | {:error, reason()} def search( @@ -145,22 +128,13 @@ defmodule ExRets do } = rets_client, %SearchArguments{} = search_arguments ) do - rets_client - |> search_fun(search_arguments) - |> Task.async() - |> Task.await(:infinity) - end - - defp search_fun(%RetsClient{} = rets_client, search_arguments) do - fn -> - search_uri = rets_client.login_response.capability_uris.search - body = SearchArguments.encode_query(search_arguments) - request = %HttpRequest{method: :post, uri: search_uri, body: body} - http_client_implementation = rets_client.http_client_implementation + search_uri = rets_client.login_response.capability_uris.search + body = SearchArguments.encode_query(search_arguments) + request = %HttpRequest{method: :post, uri: search_uri, body: body} + http_client_implementation = rets_client.http_client_implementation - with {:ok, _response, stream} <- Middleware.open_stream(rets_client, request) do - SearchResponse.parse(stream, http_client_implementation) - end + with {:ok, _response, stream} <- Middleware.open_stream(rets_client, request) do + SearchResponse.parse(stream, http_client_implementation) end end end diff --git a/lib/ex_rets/middleware.ex b/lib/ex_rets/middleware.ex index fa2c914..de7656b 100644 --- a/lib/ex_rets/middleware.ex +++ b/lib/ex_rets/middleware.ex @@ -24,7 +24,6 @@ defmodule ExRets.Middleware do @callback call(HttpRequest.t(), next(), opts()) :: HttpResponse.t() @doc since: "0.1.0" - @dialyzer {:no_contracts, open_stream: 2} @spec open_stream(RetsClient.t(), HttpRequest.t()) :: {:ok, HttpResponse.t(), HttpClient.stream()} | {:ok, HttpResponse.t()} diff --git a/lib/ex_rets/search_response.ex b/lib/ex_rets/search_response.ex index 22e827c..2c98a3d 100644 --- a/lib/ex_rets/search_response.ex +++ b/lib/ex_rets/search_response.ex @@ -96,7 +96,7 @@ defmodule ExRets.SearchResponse do columns = state.characters |> Enum.reverse() - |> Enum.join("") + |> IO.iodata_to_binary() |> CompactRecord.decode() put_in(state.rets_response.response.columns, columns) @@ -106,7 +106,7 @@ defmodule ExRets.SearchResponse do row = state.characters |> Enum.reverse() - |> Enum.join("") + |> IO.iodata_to_binary() |> CompactRecord.decode() put_in(state.rets_response.response.rows, [row | state.rets_response.response.rows]) From 25c06117f2b1950f407c2c9f83969820018fdc49 Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Mon, 26 Jun 2023 00:04:35 -0400 Subject: [PATCH 6/9] Set up Elixir, Erlang/OTP CI matrix --- .github/workflows/elixir.yml | 162 ++++++++++++++++++++++------------- mix.exs | 12 +-- 2 files changed, 111 insertions(+), 63 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 422fc8e..bf8279a 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -1,70 +1,116 @@ name: Elixir CI -on: push +# Define workflow that runs when changes are pushed to the +# `main` branch or pushed to a PR branch that targets the `main` +# branch. Change the branch name if your project uses a +# different name for the main branch like "master" or "production". +on: + push: + branches: ["main"] # adapt branch for project + pull_request: + branches: ["main"] # adapt branch for project -jobs: - compile: - runs-on: ubuntu-latest - container: - image: elixir:1.9.4-slim - env: - MIX_ENV: prod - steps: - - uses: actions/checkout@v1 - - name: install dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get --only prod - - name: compile - run: mix compile --force +# Sets the ENV `MIX_ENV` to `test` for running tests +env: + MIX_ENV: test +permissions: + contents: read + +jobs: test: - runs-on: ubuntu-latest - container: - image: elixir:1.9.4-slim - env: - MIX_ENV: test + runs-on: ubuntu-20.04 + name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + strategy: + # Specify the OTP and Elixir versions to use when building + # and running the workflow steps. + matrix: + otp: ["25.3.2.2", "26.0.1"] # Define the OTP version [required] + elixir: ["1.14.5", "1.15.0"] # Define the elixir version [required] steps: - - uses: actions/checkout@v1 - - name: install dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get --only test - - name: test - run: mix test --cover --trace - - name: archive code coverage - uses: actions/upload-artifact@v1 + # Step: Setup Elixir + Erlang image as the base. + - name: Set up Elixir + uses: erlef/setup-beam@v1 with: - name: cover - path: cover + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} - static_code_analysis: - runs-on: ubuntu-latest - container: - image: elixir:1.9.4-slim - env: - MIX_ENV: dev - steps: - - uses: actions/checkout@v1 - - name: install dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get --only dev - - name: plt cache - uses: actions/cache@v1.1.0 + # Step: Check out the code. + - name: Checkout code + uses: actions/checkout@v3 + + # Step: Define how to cache deps. Restores existing cache if present. + - name: Cache deps + id: cache-deps + uses: actions/cache@v3 + env: + cache-name: cache-elixir-deps + with: + path: deps + key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + ${{ runner.os }}-mix-${{ env.cache-name }}- + + # Step: Define how to cache the `_build` directory. After the first run, + # this speeds up tests runs a lot. This includes not re-compiling our + # project's downloaded deps every run. + - name: Cache compiled build + id: cache-build + uses: actions/cache@v3 + env: + cache-name: cache-compiled-build with: path: _build - key: ${{ runner.os }}-plt-${{ env.GITHUB_REF }} + key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} restore-keys: | - ${{ runner.os }}-plt- - - name: formatted? - run: mix format --check-formatted --dry-run - - name: credo - run: mix credo --verbose - - name: gradient + ${{ runner.os }}-mix-${{ env.cache-name }}- + ${{ runner.os }}-mix- + + # Step: Conditionally bust the cache when job is re-run. + # Sometimes, we may have issues with incremental builds that are fixed by + # doing a full recompile. In order to not waste dev time on such trivial + # issues (while also reaping the time savings of incremental builds for + # *most* day-to-day development), force a full recompile only on builds + # that are retried. + - name: Clean to rule out incremental build as a source of flakiness + if: github.run_attempt != '1' + run: | + mix deps.clean --all + mix clean + shell: sh + + # Step: Download project dependencies. If unchanged, uses + # the cached version. + - name: Install dependencies + run: mix deps.get + + # Step: Compile the project treating any warnings as errors. + # Customize this step if a different behavior is desired. + - name: Compiles without warnings + run: mix compile --warnings-as-errors + + # Step: Check that the checked in code has already been formatted. + # This step fails if something was found unformatted. + # Customize this step as desired. + - name: Check Formatting + run: mix format --check-formatted + + # Step: Execute the tests. + - name: Run tests + if: success() || failure() + run: mix test + + # Step: Run credo. + - name: Run credo + if: success() || failure() + run: mix credo + + # Step: Run dialyzer. + - name: Run dialyzer + if: success() || failure() + run: mix dialyzer + + # Step: Run gradient. + - name: Run gradient + if: success() || failure() run: mix gradient - - name: dialyzer - run: mix dialyzer --halt-exit-status diff --git a/mix.exs b/mix.exs index 89a5e43..2855b8e 100644 --- a/mix.exs +++ b/mix.exs @@ -7,13 +7,15 @@ defmodule ExRets.MixProject do [ app: :ex_rets, version: @version, - elixir: "~> 1.9", + elixir: "~> 1.14", name: "ExRets", description: "RETS client for Elixir.", start_permanent: Mix.env() == :prod, deps: deps(), docs: docs(), - package: package() + package: package(), + dialyzer: [plt_add_apps: [:ex_unit]], + preferred_cli_env: [credo: :test, dialyzer: :test, gradient: :test] ] end @@ -25,10 +27,10 @@ defmodule ExRets.MixProject do defp deps do [ - {:credo, "~> 1.1", only: :dev, runtime: false}, - {:dialyxir, "~> 1.3", only: :dev, runtime: false}, + {:credo, "~> 1.1", only: [:dev, :test], runtime: false}, + {:dialyxir, "~> 1.3", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.21", only: :dev, runtime: false}, - {:gradient, github: "esl/gradient", only: :dev, runtime: false} + {:gradient, github: "esl/gradient", only: [:dev, :test], runtime: false} ] end From 682c38eca4fc741b23acce9c28750b32f13ff8ac Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Mon, 3 Jul 2023 19:29:14 -0400 Subject: [PATCH 7/9] Use IO.chardata_to_string/1 in SearchResponse --- lib/ex_rets/search_response.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_rets/search_response.ex b/lib/ex_rets/search_response.ex index 2c98a3d..35e2d08 100644 --- a/lib/ex_rets/search_response.ex +++ b/lib/ex_rets/search_response.ex @@ -96,7 +96,7 @@ defmodule ExRets.SearchResponse do columns = state.characters |> Enum.reverse() - |> IO.iodata_to_binary() + |> IO.chardata_to_string() |> CompactRecord.decode() put_in(state.rets_response.response.columns, columns) @@ -106,7 +106,7 @@ defmodule ExRets.SearchResponse do row = state.characters |> Enum.reverse() - |> IO.iodata_to_binary() + |> IO.chardata_to_string() |> CompactRecord.decode() put_in(state.rets_response.response.rows, [row | state.rets_response.response.rows]) From 7eba36cce548b1a7ce043a3addea74292871a05d Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Fri, 7 Jul 2023 17:16:37 -0400 Subject: [PATCH 8/9] Expose RetsClient.t() to make dialyzer happy --- lib/ex_rets.ex | 11 ++++------- lib/ex_rets/rets_client.ex | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/ex_rets.ex b/lib/ex_rets.ex index 3b8c801..a9b898f 100644 --- a/lib/ex_rets.ex +++ b/lib/ex_rets.ex @@ -19,10 +19,6 @@ defmodule ExRets do require Logger - @typedoc "RETS client." - @typedoc since: "0.1.0" - @opaque client :: RetsClient.t() - @typedoc "Options for the RETS client." @typedoc since: "0.1.0" @type opts :: [timeout: integer()] @@ -45,7 +41,7 @@ defmodule ExRets do """ @doc since: "0.1.0" @spec login(Credentials.t(), opts()) :: - {:ok, client()} | {:ok, HttpResponse.t()} | {:error, reason()} + {:ok, RetsClient.t()} | {:ok, HttpResponse.t()} | {:error, reason()} def login(%Credentials{} = credentials, opts \\ []) do with {:ok, rets_client} <- new_rets_client(credentials, opts) do login_client(rets_client) @@ -97,7 +93,8 @@ defmodule ExRets do An `:error` tuple is returned for any other issues. """ @doc since: "0.1.0" - @spec logout(client()) :: {:ok, RetsResponse.t()} | {:ok, HttpResponse.t()} | {:error, reason()} + @spec logout(RetsClient.t()) :: + {:ok, RetsResponse.t()} | {:ok, HttpResponse.t()} | {:error, reason()} def logout(%RetsClient{} = rets_client) do logout_uri = rets_client.login_response.capability_uris.logout request = %HttpRequest{uri: logout_uri} @@ -118,7 +115,7 @@ defmodule ExRets do An `:error` tuple is returned for any other issues. """ @doc since: "0.1.0" - @spec search(client(), SearchArguments.t()) :: + @spec search(RetsClient.t(), SearchArguments.t()) :: {:ok, RetsResponse.t()} | {:ok, HttpResponse.t()} | {:error, reason()} def search( %RetsClient{ diff --git a/lib/ex_rets/rets_client.ex b/lib/ex_rets/rets_client.ex index bb05380..3518fb2 100644 --- a/lib/ex_rets/rets_client.ex +++ b/lib/ex_rets/rets_client.ex @@ -13,7 +13,7 @@ defmodule ExRets.RetsClient do @type t :: %__MODULE__{ credentials: Credentials.t(), http_client: HttpClient.client(), - http_client_implementation: Httpc | Mock, + http_client_implementation: module(), http_timeout: non_neg_integer() | :infinity, login_response: LoginResponse.t(), middleware: [Middleware.t()] From 7189311bb6223cf55ca1fe1c73f5a1202bd4f38d Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Sat, 29 Jul 2023 19:21:11 -0400 Subject: [PATCH 9/9] Allow ExRets.HttpClient.Httpc to work across nodes --- .gradient_ignore.exs | 4 ++-- lib/ex_rets/http_client/httpc.ex | 33 ++++++++++++++++---------------- lib/ex_rets/rets_client.ex | 2 -- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.gradient_ignore.exs b/.gradient_ignore.exs index 46ccb18..da76785 100644 --- a/.gradient_ignore.exs +++ b/.gradient_ignore.exs @@ -1,4 +1,4 @@ [ - "lib/ex_rets.ex:100", - "lib/ex_rets.ex:121" + "lib/ex_rets.ex:96", + "lib/ex_rets.ex:118" ] diff --git a/lib/ex_rets/http_client/httpc.ex b/lib/ex_rets/http_client/httpc.ex index 5e74af0..a4c02e8 100644 --- a/lib/ex_rets/http_client/httpc.ex +++ b/lib/ex_rets/http_client/httpc.ex @@ -49,6 +49,8 @@ defmodule ExRets.HttpClient.Httpc do @typedoc since: "0.1.0" @type result :: {status_line(), headers(), body()} + @error_http_client_stopped {:error, :http_client_stopped} + # Interface @impl HttpClient @@ -75,9 +77,7 @@ defmodule ExRets.HttpClient.Httpc do | {:error, ExRets.reason()} def open_stream(client, %HttpRequest{} = request, http_opts \\ []) when is_pid(client) and is_list(http_opts) do - with true <- Process.alive?(client), - {:ok, stream} <- - GenServer.start_link(__MODULE__, {client, request, http_opts}), + with {:ok, stream} <- GenServer.start_link(__MODULE__, {client, request, http_opts}), {:ok, %HttpResponse{status: 200} = response} <- GenServer.call(stream, :start_stream, :infinity) do {:ok, response, stream} @@ -96,11 +96,10 @@ defmodule ExRets.HttpClient.Httpc do @impl HttpClient @doc since: "0.1.0" def close_stream(stream) when is_pid(stream) do - if Process.alive?(stream) do - GenServer.call(stream, :cancel_stream, :infinity) - else - :ok - end + GenServer.call(stream, :cancel_stream, :infinity) + catch + # GenServer doesn't exist + :exit, _e -> :ok end @impl HttpClient @@ -134,8 +133,15 @@ defmodule ExRets.HttpClient.Httpc do from, %{client: client, http_opts: http_opts, request: request} = state ) do - {:ok, request_id} = start_async_request(client, request, http_opts) + httpc_request = HttpRequest.to_httpc(request) + http_opts = merge_default_http_opts(http_opts) + + {:ok, request_id} = + :httpc.request(request.method, httpc_request, http_opts, httpc_opts(), client) + {:noreply, %{state | from: from, request_id: request_id}} + catch + :exit, _e -> {:stop, :normal, @error_http_client_stopped, state} end def handle_call( @@ -159,6 +165,8 @@ defmodule ExRets.HttpClient.Httpc do :ok = :httpc.stream_next(httpc_stream_pid) {:noreply, %{state | from: from}} end + catch + :exit, _e -> {:stop, :normal, @error_http_client_stopped, state} end def handle_call(:cancel_stream, _from, %{client: client, request_id: request_id} = state) do @@ -213,13 +221,6 @@ defmodule ExRets.HttpClient.Httpc do end end - defp start_async_request(client, %HttpRequest{} = request, http_opts) do - httpc_request = HttpRequest.to_httpc(request) - http_opts = merge_default_http_opts(http_opts) - - :httpc.request(request.method, httpc_request, http_opts, httpc_opts(), client) - end - defp merge_default_http_opts(http_opts) do preferred_ciphers = ssl_preferred_ciphers() ciphers = :ssl.filter_cipher_suites(preferred_ciphers, []) diff --git a/lib/ex_rets/rets_client.ex b/lib/ex_rets/rets_client.ex index 3518fb2..7eb92f3 100644 --- a/lib/ex_rets/rets_client.ex +++ b/lib/ex_rets/rets_client.ex @@ -4,8 +4,6 @@ defmodule ExRets.RetsClient do alias ExRets.Credentials alias ExRets.HttpClient - alias ExRets.HttpClient.Httpc - alias ExRets.HttpClient.Mock alias ExRets.LoginResponse alias ExRets.Middleware