From 53f82bd13fc83fbd6f8f71c71da38d344930adc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ferreira?= Date: Thu, 23 Jun 2022 14:55:31 +0100 Subject: [PATCH] Adds the Natural Language Understanding API --- .../natural_language_understanding.ex | 569 ++++++++++++++++++ lib/ibmcloud/utils.ex | 13 + mix.exs | 6 +- mix.lock | 18 +- .../natural_language_understanding_test.exs | 494 +++++++++++++++ test/test_helper.exs | 1 + 6 files changed, 1091 insertions(+), 10 deletions(-) create mode 100644 lib/ibmcloud/natural_language_understanding.ex create mode 100644 test/ibmcloud/natural_language_understanding_test.exs diff --git a/lib/ibmcloud/natural_language_understanding.ex b/lib/ibmcloud/natural_language_understanding.ex new file mode 100644 index 0000000..22765ac --- /dev/null +++ b/lib/ibmcloud/natural_language_understanding.ex @@ -0,0 +1,569 @@ +defmodule IBMCloud.NaturalLanguageUnderstanding do + @moduledoc """ + IBM Natural Language Understanding API. + + - [IBM Cloud API Docs: Natural Language Understanding API](https://cloud.ibm.com/apidocs/natural-language-understanding) + """ + + import IBMCloud.Utils + + @type client :: Tesla.Client.t() + + @type analyze_params :: %{ + required(:version) => String.t(), + required(:features) => String.t(), + optional(:text) => String.t(), + optional(:html) => String.t(), + optional(:url) => String.t(), + optional(:return_analyzed_text) => boolean(), + optional(:clean) => boolean(), + optional(:xpath) => String.t(), + optional(:fallback_to_raw) => boolean(), + optional(:language) => String.t(), + optional(:categories) => %{ + optional(:explanation) => boolean(), + optional(:limit) => non_neg_integer(), + optional(:model) => String.t() + }, + optional(:classifications) => %{ + optional(:model) => String.t() + }, + optional(:concepts) => %{ + optional(:limit) => non_neg_integer() + }, + optional(:emotion) => %{ + optional(:document) => boolean(), + optional(:targets) => String.t() + }, + optional(:entities) => %{ + optional(:limit) => non_neg_integer(), + optional(:mentions) => boolean(), + optional(:model) => String.t(), + optional(:emotion) => boolean(), + optional(:sentiment) => boolean() + }, + optional(:keywords) => %{ + optional(:limit) => non_neg_integer(), + optional(:emotion) => boolean(), + optional(:sentiment) => boolean() + }, + optional(:relations) => %{ + optional(:model) => String.t() + }, + optional(:semantic_roles) => %{ + optional(:limit) => non_neg_integer(), + optional(:entities) => boolean(), + optional(:keywords) => boolean() + }, + optional(:sentiment) => %{ + optional(:document) => boolean(), + optional(:model) => String.t(), + optional(:targets) => String.t() + }, + optional(:syntax) => %{ + optional(:tokens) => %{ + optional(:lemma) => boolean(), + optional(:part_of_speech) => boolean() + }, + optional(:sentences) => boolean() + }, + optional(:limit_text_characters) => non_neg_integer() + } + + @type model_params :: %{ + required(:language) => String.t(), + required(:training_data) => String.t(), + optional(:name) => String.t(), + optional(:user_metadata) => map(), + optional(:description) => String.t(), + optional(:model_version) => String.t(), + optional(:workspace_id) => String.t(), + optional(:version_description) => String.t() + } + + @doc """ + Builds and returns an authorized client for calling NLU endpoints using a + Bearer token for authentication. + """ + @spec build_client( + bearer_token :: String.t(), + endpoint :: String.t(), + adapter :: Tesla.Client.adapter() | nil + ) :: client() + def build_client(bearer_token, endpoint, adapter \\ nil) + when is_binary(bearer_token) and is_binary(endpoint), + do: build_json_client_with_bearer(endpoint, bearer_token, adapter) + + @doc """ + Builds and returns an authorized client for calling NLU endpoints using an + API key for authentication. + """ + @spec build_api_key_client( + api_key :: String.t(), + endpoint :: String.t(), + adapter :: Tesla.Client.adapter() | nil + ) :: client() + def build_api_key_client(api_key, endpoint, adapter \\ nil) + when is_binary(api_key) and is_binary(endpoint), + do: build_json_client_with_api_key(endpoint, api_key, adapter) + + @doc """ + Analyzes raw text, HTML, or a public webpage. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#analyze) + for params and result details. + + ### Options + + * `:method`: One of `:get` (default) or `:post`, selects which HTTP method to + use when making this method call. + """ + @spec analyze( + client :: client(), + version :: String.t(), + query_or_body :: analyze_params(), + opts :: [method: :get | :post], + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def analyze(client, version, query_or_body, opts \\ [], tesla_opts \\ []) + when is_binary(version) and is_map(query_or_body) do + with {:ok, method} <- fetch_analyze_method(opts[:method]), + {:ok, %{status: 200, body: body}} <- + do_analyze(client, version, query_or_body, method, tesla_opts) do + {:ok, body} + end + end + + defp fetch_analyze_method(:get), do: {:ok, :get} + defp fetch_analyze_method(:post), do: {:ok, :post} + defp fetch_analyze_method(nil), do: {:ok, :get} + defp fetch_analyze_method(_other), do: {:error, :unknown_method} + + defp do_analyze(client, version, query, :get, tesla_opts) do + path = URI.parse("/v1/analyze") + + query = + query + |> Map.put(:version, version) + |> UriQuery.params() + |> URI.encode_query(:rfc3986) + + full_path = URI.to_string(%{path | query: query}) + + Tesla.get(client, full_path, tesla_opts) + end + + defp do_analyze(client, version, body, :post, tesla_opts) do + path = attach_version("/v1/analyze", version) + Tesla.post(client, path, body, tesla_opts) + end + + @doc """ + Lists Watson Knowledge Studio custom entities and relations models that are + deployed to your Natural Language Understanding service. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#listmodels) + for details. + """ + @spec list_models( + client :: client(), + version :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def list_models(client, version, tesla_opts \\ []) when is_binary(version) do + path = attach_version("/v1/models", version) + + with {:ok, %{status: 200, body: %{"models" => _models} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + Lists Watson Knowledge Studio custom entities and relations models that are + deployed to your Natural Language Understanding service. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#listmodels) + for details. + """ + @spec delete_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def delete_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"deleted" => _model_id} = resp}} <- + Tesla.delete(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Creates a custom sentiment model by uploading training data and + associated metadata. The model begins the training and deploying process and + is ready to use when the status is available. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#createsentimentmodel) + for details. + """ + @spec create_sentiment_model( + client :: client(), + version :: String.t(), + params :: model_params(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def create_sentiment_model(client, version, params, tesla_opts \\ []) do + path = attach_version("/v1/models/sentiment", version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.post(client, path, params, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Returns all custom sentiment models associated with this service + instance. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#listsentimentmodels) + for details. + """ + @spec list_sentiment_models( + client :: client(), + version :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def list_sentiment_models(client, version, tesla_opts \\ []) when is_binary(version) do + path = attach_version("/v1/models/sentiment", version) + + with {:ok, %{status: 200, body: %{"models" => _models} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Returns the status of the sentiment model with the given model ID. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#getsentimentmodel) + for details. + """ + @spec get_sentiment_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def get_sentiment_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/sentiment/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Overwrites the training data associated with this custom sentiment + model and retrains the model. The new model replaces the current deployment. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#updatesentimentmodel) + for details. + """ + @spec update_sentiment_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + params :: model_params(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def update_sentiment_model(client, version, model_id, params, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) and is_map(params) do + path = attach_version("/v1/models/sentiment/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.put(client, path, params, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Un-deploys the custom sentiment model with the given model ID and + deletes all associated customer data, including any training data or binary + artifacts. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#deletesentimentmodel) + for details. + """ + @spec delete_sentiment_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def delete_sentiment_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/sentiment/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"deleted" => _model_id} = resp}} <- + Tesla.delete(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Creates a custom categories model by uploading training data and + associated metadata. The model begins the training and deploying process and + is ready to use when the status is available. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#createcategoriesmodel) + for details. + """ + @spec create_categories_model( + client :: client(), + version :: String.t(), + params :: model_params(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def create_categories_model(client, version, params, tesla_opts \\ []) do + path = attach_version("/v1/models/categories", version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.post(client, path, params, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Returns all custom categories models associated with this service instance. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#listcategoriesmodels) + for details. + """ + @spec list_categories_models( + client :: client(), + version :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def list_categories_models(client, version, tesla_opts \\ []) when is_binary(version) do + path = attach_version("/v1/models/categories", version) + + with {:ok, %{status: 200, body: %{"models" => _models} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Returns the status of the categories model with the given model ID. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#getcategoriesmodel) + for details. + """ + @spec get_categories_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def get_categories_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/categories/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Overwrites the training data associated with this custom categories + model and retrains the model. The new model replaces the current deployment. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#updatecategoriesmodel) + for details. + """ + @spec update_categories_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + params :: model_params(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def update_categories_model(client, version, model_id, params, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) and is_map(params) do + path = attach_version("/v1/models/categories/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.put(client, path, params, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Un-deploys the custom categories model with the given model ID and + deletes all associated customer data, including any training data or binary + artifacts. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#deletecategoriesmodel) + for details. + """ + @spec delete_categories_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def delete_categories_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/categories/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"deleted" => _model_id} = resp}} <- + Tesla.delete(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + Creates a custom classifications model by uploading training data and + associated metadata. The model begins the training and deploying process and + is ready to use when the status is available. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#createclassificationsmodel) + for details. + """ + @spec create_classifications_model( + client :: client(), + version :: String.t(), + params :: model_params(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def create_classifications_model(client, version, params, tesla_opts \\ []) do + path = attach_version("/v1/models/classifications", version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.post(client, path, params, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Returns all custom classifications models associated with this service instance. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#listclassificationsmodels) + for details. + """ + @spec list_classifications_models( + client :: client(), + version :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def list_classifications_models(client, version, tesla_opts \\ []) when is_binary(version) do + path = attach_version("/v1/models/classifications", version) + + with {:ok, %{status: 200, body: %{"models" => _models} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + (Beta) Returns the status of the classifications model with the given model ID. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#getclassificationsmodel) + for details. + """ + @spec get_classifications_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def get_classifications_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/classifications/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.get(client, path, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + Overwrites the training data associated with this custom classifications model + and retrains the model. The new model replaces the current deployment. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#updateclassificationsmodel) + for details. + """ + @spec update_classifications_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + params :: model_params(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def update_classifications_model(client, version, model_id, params, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) and is_map(params) do + path = attach_version("/v1/models/classifications/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"model_id" => _model_id} = resp}} <- + Tesla.put(client, path, params, tesla_opts) do + {:ok, resp} + end + end + + @doc """ + Un-deploys the custom classifications model with the given model ID and + deletes all associated customer data, including any training data or binary + artifacts. + + See the [official docs](https://cloud.ibm.com/apidocs/natural-language-understanding#deleteclassificationsmodel) + for details. + """ + @spec delete_classifications_model( + client :: client(), + version :: String.t(), + model_id :: String.t(), + tesla_opts :: keyword() | nil + ) :: + {:ok, map()} | {:error, any()} + def delete_classifications_model(client, version, model_id, tesla_opts \\ []) + when is_binary(version) and is_binary(model_id) do + path = attach_version("/v1/models/classifications/" <> uri_encode(model_id), version) + + with {:ok, %{status: 200, body: %{"deleted" => _model_id} = resp}} <- + Tesla.delete(client, path, tesla_opts) do + {:ok, resp} + end + end + + defp attach_version(path, version) do + path + |> URI.parse() + |> Map.put(:query, URI.encode_query(%{version: version})) + |> URI.to_string() + end +end diff --git a/lib/ibmcloud/utils.ex b/lib/ibmcloud/utils.ex index 509ae1a..247c05f 100644 --- a/lib/ibmcloud/utils.ex +++ b/lib/ibmcloud/utils.ex @@ -22,6 +22,19 @@ defmodule IBMCloud.Utils do ) end + def build_json_client_with_api_key(endpoint, api_key, adapter) when is_binary(api_key) do + credentials = :base64.encode("apikey:#{api_key}") + + Tesla.client( + [ + {Tesla.Middleware.BaseUrl, endpoint}, + Tesla.Middleware.JSON, + {Tesla.Middleware.Headers, [{"authorization", "Basic #{credentials}"}]} + ], + adapter + ) + end + def uri_encode(val) when is_integer(val), do: to_string(val) def uri_encode(val), do: URI.encode(val, &URI.char_unreserved?/1) diff --git a/mix.exs b/mix.exs index 4eedee8..82ef603 100644 --- a/mix.exs +++ b/mix.exs @@ -32,10 +32,12 @@ defmodule IBMCloud.MixProject do [ {:jason, "~> 1.1"}, {:jose, "~> 1.10"}, - {:tesla, "~> 1.3"}, + {:tesla, "~> 1.4"}, + {:uri_query, "~> 0.1.1"}, {:credo, "~> 1.1", only: :dev, runtime: false}, {:excoveralls, "~> 0.12", only: :test}, - {:ex_doc, "~> 0.21", only: :dev, runtime: false} + {:ex_doc, "~> 0.21", only: :dev, runtime: false}, + {:faker, "~> 0.17", only: :test} ] end diff --git a/mix.lock b/mix.lock index d8c0e60..d5c099f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,22 +1,24 @@ %{ "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, "credo": {:hex, :credo, "1.4.0", "92339d4cbadd1e88b5ee43d427b639b68a11071b6f73854e33638e30a0ea11f5", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1fd3b70dce216574ce3c18bdf510b57e7c4c85c2ec9cad4bff854abaf7e58658"}, "earmark_parser": {:hex, :earmark_parser, "1.4.10", "6603d7a603b9c18d3d20db69921527f82ef09990885ed7525003c7fe7dc86c56", [:mix], [], "hexpm", "8e2d5370b732385db2c9b22215c3f59c84ac7dda7ed7e544d7c459496ae519c0"}, "ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"}, "excoveralls": {:hex, :excoveralls, "0.13.2", "5ca05099750c086f144fcf75842c363fc15d7d9c6faa7ad323d010294ced685e", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1e7ed75c158808a5a8f019d3ad63a5efe482994f2f8336c0a8c77d2f0ab152ce"}, - "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, - "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, - "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, "jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"}, "makeup": {:hex, :makeup, "1.0.4", "6d1936d21585cd258aeadf7728a572ae2a6017a909349a12270c1e460e360f49", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a45dc0440b9ff1aca4883bedcae1f5939ec3b1f6e28026a15147ed1a45d897cd"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "1.4.0", "5066f14944b470286146047d2f73518cf5cca82f8e4815cf35d196b58cf07c47", [:mix], [], "hexpm", "75fa42c4228ea9a23f70f123c74ba7cece6a03b1fd474fe13f6a7a85c6ea4ff6"}, + "mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "nimble_parsec": {:hex, :nimble_parsec, "1.0.0", "54840f7a89aa3443c9280056742c329259d02787ef1da651f8591706518ceec6", [:mix], [], "hexpm", "6611313377408b6f19d4eddb1a53a342c70e336a21e9d09c38a9230775f1150d"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, - "tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"}, + "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "uri_query": {:hex, :uri_query, "0.1.2", "ae35b83b472f3568c2c159eee3f3ccf585375d8a94fb5382db1ea3589e75c3b4", [:mix], [], "hexpm", "e3bc81816c98502c36498b9b2f239b89c71ce5eadfff7ceb2d6c0a2e6ae2ea0c"}, } diff --git a/test/ibmcloud/natural_language_understanding_test.exs b/test/ibmcloud/natural_language_understanding_test.exs new file mode 100644 index 0000000..be576f2 --- /dev/null +++ b/test/ibmcloud/natural_language_understanding_test.exs @@ -0,0 +1,494 @@ +defmodule IBMCloud.NaturalLanguageUnderstandingTest do + use ExUnit.Case, async: true + + alias IBMCloud.NaturalLanguageUnderstanding, as: NLU + + @test_endpoint "https://api.eu-de.natural-language-understanding.watson.cloud.ibm.com/instances/88888888-4444-4444-4444-121212121212" + @test_api_key "1234567890" + + setup do + client = NLU.build_api_key_client(@test_api_key, @test_endpoint, Tesla.Mock) + {:ok, client: client} + end + + describe "build_client/3" do + test "creates an authorized client for the given endpoint and bearer token" do + client = NLU.build_client("test-token", @test_endpoint, Tesla.Mock) + + assert match?(%Tesla.Client{}, client) + assert Tesla.Client.adapter(client) == Tesla.Mock + + middlewares = Tesla.Client.middleware(client) + + base_url = Keyword.fetch!(middlewares, Tesla.Middleware.BaseUrl) + assert base_url == @test_endpoint + + headers = Keyword.fetch!(middlewares, Tesla.Middleware.Headers) + + assert {_, auth} = Enum.find(headers, fn {key, _} -> key == "authorization" end) + assert auth == "Bearer test-token" + end + end + + describe "build_api_key_client/3" do + test "creates an authorized client for the given endpoint and API key", %{client: client} do + assert match?(%Tesla.Client{}, client) + assert Tesla.Client.adapter(client) == Tesla.Mock + + middlewares = Tesla.Client.middleware(client) + + base_url = Keyword.fetch!(middlewares, Tesla.Middleware.BaseUrl) + assert base_url == @test_endpoint + + headers = Keyword.fetch!(middlewares, Tesla.Middleware.Headers) + + assert {_, auth} = Enum.find(headers, fn {key, _} -> key == "authorization" end) + assert auth == "Basic YXBpa2V5OjEyMzQ1Njc4OTA=" + end + end + + describe "analyze/5" do + test "[GET] calls the analyze endpoint and returns results", %{client: client} do + Tesla.Mock.mock(fn + %{method: :get} = env -> + uri = URI.parse(env.url) + query = URI.decode_query(uri.query) + + assert uri.path == "/instances/88888888-4444-4444-4444-121212121212/v1/analyze" + + assert query == %{ + "version" => "2022-04-07", + "text" => "This is a horrible story!", + "features[sentiment][document]" => "true" + } + + %Tesla.Env{ + status: 200, + body: %{ + "sentiment" => %{ + "document" => %{ + "score" => -0.989674, + "label" => "negative" + } + }, + "language" => "en" + } + } + end) + + assert {:ok, response} = + NLU.analyze(client, "2022-04-07", %{ + text: "This is a horrible story!", + features: %{sentiment: %{document: true}} + }) + + assert response["language"] == "en" + + assert response["sentiment"] == %{ + "document" => %{ + "score" => -0.989674, + "label" => "negative" + } + } + end + + test "[POST] calls the analyze endpoint and returns results", %{client: client} do + Tesla.Mock.mock(fn + %{method: :post} = env -> + uri = URI.parse(env.url) + query = URI.decode_query(uri.query) + + assert uri.path == "/instances/88888888-4444-4444-4444-121212121212/v1/analyze" + assert query == %{"version" => "2022-04-07"} + + assert env.body == + Jason.encode!(%{ + text: "This is a horrible story!", + features: %{ + sentiment: %{ + document: true + } + } + }) + + %Tesla.Env{ + status: 200, + body: %{ + "sentiment" => %{ + "document" => %{ + "score" => -0.989674, + "label" => "negative" + } + }, + "language" => "en" + } + } + end) + + assert {:ok, response} = + NLU.analyze( + client, + "2022-04-07", + %{ + text: "This is a horrible story!", + features: %{sentiment: %{document: true}} + }, + method: :post + ) + + assert response["language"] == "en" + + assert response["sentiment"] == %{ + "document" => %{ + "score" => -0.989674, + "label" => "negative" + } + } + end + end + + describe "list_models/3" do + test "fetches a list of deployed models from the API", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :get, url: @test_endpoint <> "/v1/models?version=2022-04-07"} -> + %Tesla.Env{status: 200, body: %{"models" => [fake_model]}} + end) + + assert {:ok, response} = NLU.list_models(client, "2022-04-07") + assert [res_model] = response["models"] + assert res_model == fake_model + end + end + + describe "delete_model/4" do + test "calls the API to DELETE a model by ID", %{client: client} do + model_id = Faker.UUID.v4() + url = @test_endpoint <> "/v1/models/#{model_id}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :delete, url: ^url} -> + %Tesla.Env{status: 200, body: %{"deleted" => model_id}} + end) + + assert {:ok, response} = NLU.delete_model(client, "2022-04-07", model_id) + assert response["deleted"] == model_id + end + end + + describe "create_sentiment_model/4" do + test "calls the API to create a model", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :post, url: @test_endpoint <> "/v1/models/sentiment?version=2022-04-07"} -> + response = + Map.merge(fake_model, %{ + "created" => now_iso(), + "features" => ["sentiment"], + "last_trained" => nil, + "last_deployed" => nil, + "notices" => [] + }) + + %Tesla.Env{status: 200, body: response} + end) + + params = Map.put(fake_model, :training_data, "@sentiment_data.csv;type=text/csv") + assert {:ok, response} = NLU.create_sentiment_model(client, "2022-04-07", params) + + assert response["model_id"] == fake_model["model_id"] + assert response["features"] == ["sentiment"] + end + end + + describe "list_sentiment_models/3" do + test "fetches a list of sentiment models from the API", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :get, url: @test_endpoint <> "/v1/models/sentiment?version=2022-04-07"} -> + %Tesla.Env{status: 200, body: %{"models" => [fake_model]}} + end) + + assert {:ok, response} = NLU.list_sentiment_models(client, "2022-04-07") + assert [res_model] = response["models"] + assert res_model == fake_model + end + end + + describe "get_sentiment_model/4" do + test "fetches a sentiment model by ID from the API", %{client: client} do + fake_model = gen_model() + url = @test_endpoint <> "/v1/models/sentiment/#{fake_model["model_id"]}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :get, url: ^url} -> + %Tesla.Env{status: 200, body: fake_model} + end) + + assert {:ok, response} = + NLU.get_sentiment_model(client, "2022-04-07", fake_model["model_id"]) + + assert response == fake_model + end + end + + describe "update_sentiment_model/5" do + test "updates a sentiment model by ID from the API", %{client: client} do + fake_model = gen_model() + url = @test_endpoint <> "/v1/models/sentiment/#{fake_model["model_id"]}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :put, url: ^url, body: body} -> + assert body == "{\"name\":\"Something new\"}" + %Tesla.Env{status: 200, body: %{fake_model | "name" => "Something new"}} + end) + + assert {:ok, response} = + NLU.update_sentiment_model( + client, + "2022-04-07", + fake_model["model_id"], + %{name: "Something new"} + ) + + assert response == %{fake_model | "name" => "Something new"} + end + end + + describe "delete_sentiment_model/4" do + test "calls the API to DELETE a sentiment model by ID", %{client: client} do + model_id = Faker.UUID.v4() + url = @test_endpoint <> "/v1/models/sentiment/#{model_id}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :delete, url: ^url} -> + %Tesla.Env{status: 200, body: %{"deleted" => model_id}} + end) + + assert {:ok, response} = NLU.delete_sentiment_model(client, "2022-04-07", model_id) + assert response["deleted"] == model_id + end + end + + describe "create_categories_model/4" do + test "calls the API to create a model", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :post, url: @test_endpoint <> "/v1/models/categories?version=2022-04-07"} -> + response = + Map.merge(fake_model, %{ + "created" => now_iso(), + "features" => ["categories"], + "last_trained" => nil, + "last_deployed" => nil, + "notices" => [] + }) + + %Tesla.Env{status: 200, body: response} + end) + + params = Map.put(fake_model, :training_data, "@categories_data.csv;type=text/csv") + assert {:ok, response} = NLU.create_categories_model(client, "2022-04-07", params) + + assert response["model_id"] == fake_model["model_id"] + assert response["features"] == ["categories"] + end + end + + describe "list_categories_models/3" do + test "fetches a list of categories models from the API", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :get, url: @test_endpoint <> "/v1/models/categories?version=2022-04-07"} -> + %Tesla.Env{status: 200, body: %{"models" => [fake_model]}} + end) + + assert {:ok, response} = NLU.list_categories_models(client, "2022-04-07") + assert [res_model] = response["models"] + assert res_model == fake_model + end + end + + describe "get_categories_model/4" do + test "fetches a categories model by ID from the API", %{client: client} do + fake_model = gen_model() + url = @test_endpoint <> "/v1/models/categories/#{fake_model["model_id"]}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :get, url: ^url} -> + %Tesla.Env{status: 200, body: fake_model} + end) + + assert {:ok, response} = + NLU.get_categories_model(client, "2022-04-07", fake_model["model_id"]) + + assert response == fake_model + end + end + + describe "update_categories_model/5" do + test "updates a categories model by ID from the API", %{client: client} do + fake_model = gen_model() + url = @test_endpoint <> "/v1/models/categories/#{fake_model["model_id"]}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :put, url: ^url, body: body} -> + assert body == "{\"name\":\"Something new\"}" + %Tesla.Env{status: 200, body: %{fake_model | "name" => "Something new"}} + end) + + assert {:ok, response} = + NLU.update_categories_model( + client, + "2022-04-07", + fake_model["model_id"], + %{name: "Something new"} + ) + + assert response == %{fake_model | "name" => "Something new"} + end + end + + describe "delete_categories_model/4" do + test "calls the API to DELETE a categories model by ID", %{client: client} do + model_id = Faker.UUID.v4() + url = @test_endpoint <> "/v1/models/categories/#{model_id}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :delete, url: ^url} -> + %Tesla.Env{status: 200, body: %{"deleted" => model_id}} + end) + + assert {:ok, response} = NLU.delete_categories_model(client, "2022-04-07", model_id) + assert response["deleted"] == model_id + end + end + + describe "create_classifications_model/4" do + test "calls the API to create a model", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :post, url: @test_endpoint <> "/v1/models/classifications?version=2022-04-07"} -> + response = + Map.merge(fake_model, %{ + "created" => now_iso(), + "features" => ["classifications"], + "last_trained" => nil, + "last_deployed" => nil, + "notices" => [] + }) + + %Tesla.Env{status: 200, body: response} + end) + + params = Map.put(fake_model, :training_data, "@classifications_data.csv;type=text/csv") + assert {:ok, response} = NLU.create_classifications_model(client, "2022-04-07", params) + + assert response["model_id"] == fake_model["model_id"] + assert response["features"] == ["classifications"] + end + end + + describe "list_classifications_models/3" do + test "fetches a list of classifications models from the API", %{client: client} do + fake_model = gen_model() + + Tesla.Mock.mock(fn + %{method: :get, url: @test_endpoint <> "/v1/models/classifications?version=2022-04-07"} -> + %Tesla.Env{status: 200, body: %{"models" => [fake_model]}} + end) + + assert {:ok, response} = NLU.list_classifications_models(client, "2022-04-07") + assert [res_model] = response["models"] + assert res_model == fake_model + end + end + + describe "get_classifications_model/4" do + test "fetches a classifications model by ID from the API", %{client: client} do + fake_model = gen_model() + + url = + @test_endpoint <> + "/v1/models/classifications/#{fake_model["model_id"]}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :get, url: ^url} -> + %Tesla.Env{status: 200, body: fake_model} + end) + + assert {:ok, response} = + NLU.get_classifications_model(client, "2022-04-07", fake_model["model_id"]) + + assert response == fake_model + end + end + + describe "update_classifications_model/5" do + test "updates a classifications model by ID from the API", %{client: client} do + fake_model = gen_model() + + url = + @test_endpoint <> + "/v1/models/classifications/#{fake_model["model_id"]}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :put, url: ^url, body: body} -> + assert body == "{\"name\":\"Something new\"}" + %Tesla.Env{status: 200, body: %{fake_model | "name" => "Something new"}} + end) + + assert {:ok, response} = + NLU.update_classifications_model( + client, + "2022-04-07", + fake_model["model_id"], + %{name: "Something new"} + ) + + assert response == %{fake_model | "name" => "Something new"} + end + end + + describe "delete_classifications_model/4" do + test "calls the API to DELETE a classifications model by ID", %{client: client} do + model_id = Faker.UUID.v4() + url = @test_endpoint <> "/v1/models/classifications/#{model_id}?version=2022-04-07" + + Tesla.Mock.mock(fn + %{method: :delete, url: ^url} -> + %Tesla.Env{status: 200, body: %{"deleted" => model_id}} + end) + + assert {:ok, response} = NLU.delete_classifications_model(client, "2022-04-07", model_id) + assert response["deleted"] == model_id + end + end + + defp gen_model do + version = Faker.App.semver() + + %{ + "workspace_id" => Faker.UUID.v4(), + "version_description" => "Final version", + "model_version" => version, + "version" => version, + "status" => + Enum.random(["starting", "training", "deploying", "available", "error", "deleted"]), + "notices" => [], + "name" => Faker.App.name() <> " Model", + "model_id" => Faker.UUID.v4(), + "language" => "en", + "description" => "A test model", + "created" => now_iso() + } + end + + defp now_iso, do: DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601() +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e..acc2de6 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,2 @@ ExUnit.start() +Faker.start()