diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2653bad..159f6f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,6 @@ jobs: - name: Run integration tests run: | - docker-compose up -d + docker compose up -d mix test --only integration - docker-compose down --remove-orphans + docker compose down --remove-orphans diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/lib/ex_iceberg/catalog.ex b/lib/ex_iceberg/catalog.ex index ccc400d..f411c57 100644 --- a/lib/ex_iceberg/catalog.ex +++ b/lib/ex_iceberg/catalog.ex @@ -1,8 +1,9 @@ defmodule ExIceberg.Catalog do - @callback create_namespace(t(), String.t(), map()) :: :ok | {:error, String.t()} + @callback create_namespace(t(), String.t(), map()) :: {:ok, t(), map()} | {:error, String.t()} + @callback list_namespaces(t()) :: {:ok, t(), list(String.t())} | {:error, String.t()} @type t :: %{ name: String.t(), - properties: map() + config: ExIceberg.Config.t() } end diff --git a/lib/ex_iceberg/catalog/rest_catalog.ex b/lib/ex_iceberg/catalog/rest_catalog.ex deleted file mode 100644 index a7c7d9c..0000000 --- a/lib/ex_iceberg/catalog/rest_catalog.ex +++ /dev/null @@ -1,77 +0,0 @@ -defmodule ExIceberg.Catalog.RestCatalog do - @moduledoc """ - Module to interact with the REST catalog of Apache Iceberg. - """ - - @behaviour ExIceberg.Catalog - - defstruct name: nil, properties: %{}, http_client: ExIceberg.ReqClient - - @type t :: %__MODULE__{ - name: String.t(), - properties: map(), - http_client: module() - } - - @doc """ - Initializes a new RestCatalog. - - ## Parameters - - - `name`: The name of the catalog - - `properties`: A map of properties for the catalog - - ## Examples - - iex> ExIceberg.Catalog.RestCatalog.new("my_catalog", %{"uri" => "http://localhost:8181"}) - %ExIceberg.Catalog.RestCatalog{ - name: "my_catalog", - properties: %{"uri" => "http://localhost:8181"}, - http_client: ExIceberg.ReqClient - } - """ - def new(name, properties, http_client \\ ExIceberg.ReqClient) do - %__MODULE__{ - name: name, - properties: properties, - http_client: http_client - } - end - - @doc """ - Creates a new namespace in the catalog. - - ## Parameters - - - `catalog`: The catalog struct - - `namespace`: The name of the namespace - - `properties`: A map of properties for the namespace - - ## Examples - - iex> catalog = ExIceberg.Catalog.RestCatalog.new("my_catalog", %{"uri" => "http://localhost:8181"}) - iex> ExIceberg.Catalog.RestCatalog.create_namespace(catalog, "new_namespace", %{"property" => "value"}) - :ok - """ - @impl true - def create_namespace( - %__MODULE__{properties: %{"uri" => uri}, http_client: http_client} = _catalog, - namespace, - properties - ) do - url = uri <> "/v1/namespaces" - body = %{"namespace" => [namespace], "properties" => properties} - headers = [{"Content-Type", "application/json"}] - - case http_client.request(:post, url, body, headers) do - {:ok, _body} -> - :ok - - {:error, %Req.Response{status: status, body: %{"error" => %{"message" => message}}}} -> - {:error, "Request failed with status #{status}: #{message}"} - - {:error, %Req.TransportError{reason: reason}} -> - {:error, "HTTP request failed: #{inspect(reason)}"} - end - end -end diff --git a/lib/ex_iceberg/config.ex b/lib/ex_iceberg/config.ex new file mode 100644 index 0000000..2838fda --- /dev/null +++ b/lib/ex_iceberg/config.ex @@ -0,0 +1,2 @@ +defmodule ExIceberg.Config do +end diff --git a/lib/ex_iceberg/glue/catalog.ex b/lib/ex_iceberg/glue/catalog.ex new file mode 100644 index 0000000..691b58b --- /dev/null +++ b/lib/ex_iceberg/glue/catalog.ex @@ -0,0 +1,7 @@ +defmodule ExIceberg.Glue.Catalog do + @moduledoc """ + Module to interact with the Glue catalog. + """ + + # @behaviour ExIceberg.Catalog +end diff --git a/lib/ex_iceberg/glue/client.ex b/lib/ex_iceberg/glue/client.ex new file mode 100644 index 0000000..1349346 --- /dev/null +++ b/lib/ex_iceberg/glue/client.ex @@ -0,0 +1,2 @@ +defmodule ExIceberg.Glue.Client do +end diff --git a/lib/ex_iceberg/glue/config.ex b/lib/ex_iceberg/glue/config.ex new file mode 100644 index 0000000..8347a77 --- /dev/null +++ b/lib/ex_iceberg/glue/config.ex @@ -0,0 +1,2 @@ +defmodule ExIceberg.Glue.Config do +end diff --git a/lib/ex_iceberg/http_client.ex b/lib/ex_iceberg/http_client.ex deleted file mode 100644 index e4f8ed3..0000000 --- a/lib/ex_iceberg/http_client.ex +++ /dev/null @@ -1,3 +0,0 @@ -defmodule ExIceberg.HTTPClient do - @callback request(atom(), String.t(), map(), list()) :: {:ok, any()} | {:error, any()} -end diff --git a/lib/ex_iceberg/req_client.ex b/lib/ex_iceberg/req_client.ex deleted file mode 100644 index 99f70cf..0000000 --- a/lib/ex_iceberg/req_client.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule ExIceberg.ReqClient do - @behaviour ExIceberg.HTTPClient - - def request(method, url, body, headers) do - req = - Req.new( - method: method, - url: url, - body: Jason.encode!(body), - headers: headers - ) - - case Req.request(req) do - {:ok, %Req.Response{status: status, body: body}} when status in 200..299 -> - {:ok, body} - - {:ok, %Req.Response{status: status, body: body}} -> - {:error, %Req.Response{status: status, body: body}} - - {:error, %Req.TransportError{reason: reason}} -> - {:error, %Req.TransportError{reason: reason}} - end - end -end diff --git a/lib/ex_iceberg/rest/catalog.ex b/lib/ex_iceberg/rest/catalog.ex new file mode 100644 index 0000000..b9657bb --- /dev/null +++ b/lib/ex_iceberg/rest/catalog.ex @@ -0,0 +1,63 @@ +defmodule ExIceberg.Rest.Catalog do + @moduledoc """ + Module to interact with the REST catalog of Apache Iceberg. + """ + + alias ExIceberg.Rest.Client + alias ExIceberg.Rest.Config + + @behaviour ExIceberg.Catalog + + defstruct name: nil, config: %Config{} + + @type t :: %__MODULE__{name: String.t(), config: Config.t()} + + alias __MODULE__ + + def new(name, config) do + %__MODULE__{name: name, config: Config.new(config)} + |> authenticate() + |> get_config() + end + + @impl true + def create_namespace( + %__MODULE__{config: config} = catalog, + namespace, + properties + ) do + body = %{"namespace" => [namespace], "properties" => properties} + + case Client.request(:create_namespace, config: config, json: body) do + {:ok, response} -> {:ok, catalog, response} + {:error, reason} -> {:error, catalog, reason} + end + end + + @impl true + def list_namespaces(%Catalog{config: config} = catalog) do + case Client.request(:list_namespace, config: config) do + {:ok, response} -> {:ok, catalog, response} + {:error, reason} -> {:error, catalog, reason} + end + end + + defp authenticate(%Catalog{config: %{credential: cred}} = catalog) when cred != nil do + {:ok, token} = Client.request(:auth, config: catalog.config) + %{catalog | config: %{catalog.config | token: token}} + end + + defp authenticate(catalog), do: catalog + + defp get_config(%Catalog{config: config} = catalog) do + Client.request(:get_config, config: config) + + # TODO: Parse config and merge + # (default_config (from api) + user_config + override_config(from api)) + # We need to list all the possible configurations. I know that in the + # other implementations, it's all dynamic, but I believe that with explicit configuration, + # we can have better control over the code. + + catalog + end +end diff --git a/lib/ex_iceberg/rest/client.ex b/lib/ex_iceberg/rest/client.ex new file mode 100644 index 0000000..7bf4b25 --- /dev/null +++ b/lib/ex_iceberg/rest/client.ex @@ -0,0 +1,50 @@ +defmodule ExIceberg.Rest.Client do + alias ExIceberg.Rest.Config + + def new(options \\ []) when is_list(options) do + base_url = build_base_url(options[:config]) + + Req.new(base_url: base_url) + |> Req.Request.register_options([:config]) + |> Req.Request.merge_options(config: options[:config]) + |> Req.Request.merge_options(options) + |> Req.Request.prepend_request_steps(req_rest_catalog_auth: &auth/1) + end + + def request(action, options \\ []) do + Req.request(new(options), endpoint(action)) + |> parse_response() + end + + defp auth(%{config: %{token: token}} = request) do + Req.Request.merge_options(request, auth: {:bearer, token}) + end + + defp auth(request), do: request + + defp build_base_url(%{uri: uri, prefix: prefix}) when prefix != nil do + uri <> "/v1/" <> prefix + end + + defp build_base_url(%Config{uri: uri}) do + uri <> "/v1/" + end + + # TODO: Add options for exception handling + defp endpoint(:auth), do: [url: "/oauth/tokens", method: :post] + defp endpoint(:get_config), do: [url: "/config", method: :get] + defp endpoint(:list_namespace), do: [url: "/namespaces", method: :get] + defp endpoint(:create_namespace), do: [url: "/namespaces", method: :post] + + defp parse_response({:ok, %Req.Response{status: 200, body: body}}) do + {:ok, body} + end + + defp parse_response({:ok, %Req.Response{status: status}}) when status >= 400 do + {:error, "Request failed with status #{status}"} + end + + defp parse_response({:error, reason}) do + {:error, reason} + end +end diff --git a/lib/ex_iceberg/rest/config.ex b/lib/ex_iceberg/rest/config.ex new file mode 100644 index 0000000..2602513 --- /dev/null +++ b/lib/ex_iceberg/rest/config.ex @@ -0,0 +1,7 @@ +defmodule ExIceberg.Rest.Config do + defstruct uri: nil, prefix: nil, token: nil + + def new(options) do + struct(__MODULE__, options) + end +end diff --git a/mix.exs b/mix.exs index f2df7e9..a03ff32 100644 --- a/mix.exs +++ b/mix.exs @@ -13,8 +13,7 @@ defmodule ExIceberg.MixProject do elixir: "~> 1.16", start_permanent: Mix.env() == :prod, preferred_cli_env: [ - "test.all": :test, - "hex.publish": :docs + "test.all": :test ], deps: deps(), aliases: aliases(), @@ -38,7 +37,6 @@ defmodule ExIceberg.MixProject do [ {:req, "~> 0.5"}, {:jason, "~> 1.2"}, - {:mox, "~> 1.0", only: :test}, {:ex_doc, "~> 0.34", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index d2f6590..e7f20f4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,19 +1,18 @@ %{ - "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, - "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, + "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.40", "f3534689f6b58f48aa3a9ac850d4f05832654fe257bf0549c08cc290035f70d5", [:mix], [], "hexpm", "cdb34f35892a45325bad21735fadb88033bcb7c4c296a999bde769783f53e46a"}, + "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, - "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, + "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [: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", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, - "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, - "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "req": {:hex, :req, "0.5.0", "6d8a77c25cfc03e06a439fb12ffb51beade53e3fe0e2c5e362899a18b50298b3", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "dda04878c1396eebbfdec6db6f3d4ca609e5c8846b7ee88cc56eb9891406f7a3"}, + "req": {:hex, :req, "0.5.1", "90584216d064389a4ff2d4279fe2c11ff6c812ab00fa01a9fb9d15457f65ba70", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7ea96a1a95388eb0fefa92d89466cdfedba24032794e5c1147d78ec90db7edca"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/test/integration/catalog/rest_catalog_integration_test.exs b/test/integration/catalog/rest_catalog_integration_test.exs index e00230a..14d4447 100644 --- a/test/integration/catalog/rest_catalog_integration_test.exs +++ b/test/integration/catalog/rest_catalog_integration_test.exs @@ -1,6 +1,6 @@ -defmodule ExIceberg.Catalog.RestCatalogIntegrationTest do +defmodule ExIceberg.Catalog.RestIntegrationTest do use ExUnit.Case, async: true - alias ExIceberg.Catalog.RestCatalog + alias ExIceberg.Rest.Catalog @moduletag :integration @@ -11,19 +11,46 @@ defmodule ExIceberg.Catalog.RestCatalogIntegrationTest do "#{base}_#{hash}" end - test "successfully creates a new namespace" do - namespace = generate_unique_name("some_namespace") - catalog = RestCatalog.new("my_catalog", %{"uri" => @test_uri}) - assert :ok == RestCatalog.create_namespace(catalog, namespace, %{}) + describe "create_namespace/3" do + test "successfully creates a new namespace" do + namespace = generate_unique_name("some_namespace") + catalog = Catalog.new("my_catalog", %{uri: @test_uri}) + + assert {:ok, %Catalog{} = _catalog, response} = + Catalog.create_namespace(catalog, namespace, %{}) + + assert response == %{ + "namespace" => [namespace], + "properties" => %{"location" => "s3://icebergdata/demo/#{namespace}"} + } + end + + test "fails to create a new namespace due to conflict" do + namespace = generate_unique_name("examples") + + catalog = Catalog.new("my_catalog", %{uri: @test_uri}) + + Catalog.create_namespace(catalog, namespace, %{}) + + assert {:error, %Catalog{} = _catalog, response} = + Catalog.create_namespace(catalog, namespace, %{}) + + assert response == + "Request failed with status 409" + end end - test "fails to create a new namespace due to conflict" do - namespace = generate_unique_name("examples") - catalog = RestCatalog.new("my_catalog", %{"uri" => @test_uri}) + describe "list_namespaces/1" do + test "successfully lists namespaces" do + namespace = generate_unique_name("some_namespace") + catalog = Catalog.new("my_catalog", %{uri: @test_uri}) + + Catalog.create_namespace(catalog, namespace, %{}) - assert :ok == RestCatalog.create_namespace(catalog, namespace, %{}) + assert {:ok, %Catalog{} = _catalog, %{"namespaces" => namespaces}} = + Catalog.list_namespaces(catalog) - assert {:error, "Request failed with status 409: Namespace already exists: #{namespace}"} == - RestCatalog.create_namespace(catalog, namespace, %{}) + assert [namespace] in namespaces + end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index a46bd89..be24fa7 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,2 @@ ExUnit.start(exclude: [:integration]) -Mox.defmock(ExIceberg.MockHTTPClient, for: ExIceberg.HTTPClient) Application.ensure_all_started(:mox) diff --git a/test/unit/catalog/rest_catalog_test.exs b/test/unit/catalog/rest_catalog_test.exs deleted file mode 100644 index 38ec4e2..0000000 --- a/test/unit/catalog/rest_catalog_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -defmodule ExIceberg.Catalog.RestCatalogTest do - use ExUnit.Case, async: true - import Mox - alias ExIceberg.Catalog.RestCatalog - - setup :verify_on_exit! - - @test_uri "http://localhost:8181" - @headers [{"Content-Type", "application/json"}] - - describe "create_namespace/3" do - test "successfully creates a new namespace" do - ExIceberg.MockHTTPClient - |> expect(:request, fn :post, - @test_uri <> "/v1/namespaces", - %{"namespace" => ["some_namespace"], "properties" => %{}}, - @headers -> - {:ok, %Req.Response{status: 200, body: %{}}} - end) - - catalog = RestCatalog.new("my_catalog", %{"uri" => @test_uri}, ExIceberg.MockHTTPClient) - assert :ok == RestCatalog.create_namespace(catalog, "some_namespace", %{}) - end - - test "fails to create a new namespace due to conflict" do - ExIceberg.MockHTTPClient - |> expect(:request, fn :post, - @test_uri <> "/v1/namespaces", - %{"namespace" => ["examples"], "properties" => %{}}, - @headers -> - {:error, - %Req.Response{ - status: 409, - body: %{ - "error" => %{ - "message" => - "Namespace already exists: examples in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e", - "type" => "AlreadyExistsException", - "code" => 409 - } - } - }} - end) - - catalog = RestCatalog.new("my_catalog", %{"uri" => @test_uri}, ExIceberg.MockHTTPClient) - - assert {:error, - "Request failed with status 409: Namespace already exists: examples in warehouse 8bcb0838-50fc-472d-9ddb-8feb89ef5f1e"} == - RestCatalog.create_namespace(catalog, "examples", %{}) - end - end -end