From 03e5d962296e7d3617680ef307fec995e96a7387 Mon Sep 17 00:00:00 2001 From: Jared Hoyt Date: Thu, 22 Aug 2024 15:39:49 -0700 Subject: [PATCH 1/3] feat: Add `Metrics.set/1` resource action Also adds `Metrics.flush/0` and resource action param validation. --- lib/kickplan/requests/metrics/set.ex | 21 +++++++++++++++ lib/kickplan/resource/metrics.ex | 24 +++++++++++++++++ mix.exs | 2 ++ mix.lock | 5 ++++ test/cassettes/features/resolve-all.json | 34 ++++++++++++++++++++++++ test/cassettes/features/resolve.json | 34 ++++++++++++++++++++++++ test/cassettes/metrics/flush.json | 33 +++++++++++++++++++++++ test/cassettes/metrics/set.json | 33 +++++++++++++++++++++++ test/kickplan/resource/features_test.exs | 28 +++++++++++++++++++ test/kickplan/resource/metrics_test.exs | 33 +++++++++++++++++++++++ test/support/vcr_case.ex | 18 +++++++++++++ 11 files changed, 265 insertions(+) create mode 100644 lib/kickplan/requests/metrics/set.ex create mode 100644 lib/kickplan/resource/metrics.ex create mode 100644 test/cassettes/features/resolve-all.json create mode 100644 test/cassettes/features/resolve.json create mode 100644 test/cassettes/metrics/flush.json create mode 100644 test/cassettes/metrics/set.json create mode 100644 test/kickplan/resource/features_test.exs create mode 100644 test/kickplan/resource/metrics_test.exs create mode 100644 test/support/vcr_case.ex diff --git a/lib/kickplan/requests/metrics/set.ex b/lib/kickplan/requests/metrics/set.ex new file mode 100644 index 0000000..db37740 --- /dev/null +++ b/lib/kickplan/requests/metrics/set.ex @@ -0,0 +1,21 @@ +defmodule Kickplan.Requests.Metrics.Set do + @moduledoc """ + TODO + """ + + @options NimbleOptions.new!( + key: [ + type: :string, + required: true + ], + value: [ + type: {:or, [:float, :integer]}, + required: true + ], + account_key: [type: :string], + idempotency_key: [type: :string], + time: [type: :any] + ) + + def options, do: @options +end diff --git a/lib/kickplan/resource/metrics.ex b/lib/kickplan/resource/metrics.ex new file mode 100644 index 0000000..08d5abb --- /dev/null +++ b/lib/kickplan/resource/metrics.ex @@ -0,0 +1,24 @@ +defmodule Kickplan.Metrics do + @moduledoc """ + TODO + """ + + alias Kickplan.{Client, Requests} + + def flush do + with {:ok, resp} <- Client.post("metrics/flush") do + {:ok, resp.success?} + end + end + + def set(opts \\ %{}) do + with {:ok, params} <- validate(opts, Requests.Metrics.Set), + {:ok, resp} <- Client.post("metrics/set", params) do + {:ok, resp.success?} + end + end + + defp validate(opts, request) do + NimbleOptions.validate(opts, request.options()) + end +end diff --git a/mix.exs b/mix.exs index d433f68..d64d0a5 100644 --- a/mix.exs +++ b/mix.exs @@ -26,6 +26,7 @@ defmodule Kickplan.MixProject do defp deps do [ + {:exvcr, "~> 0.11", only: :test}, {:finch, "~> 0.6", optional: true}, {:hackney, "~> 1.9", optional: true}, {:jason, "~> 1.0", optional: true}, @@ -36,6 +37,7 @@ defmodule Kickplan.MixProject do {:ex_doc, ">= 0.0.0", only: [:dev], runtime: false}, {:gettext, ">= 0.0.0", only: [:dev], runtime: false}, {:mix_audit, ">= 0.0.0", only: [:dev], runtime: false}, + {:nimble_options, "~> 1.1.0"}, {:sobelow, ">= 0.0.0", only: [:dev], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 8b23312..dd3bd44 100644 --- a/mix.lock +++ b/mix.lock @@ -11,6 +11,9 @@ "ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"}, "ex_doc": {:hex, :ex_doc, "0.34.2", "13eedf3844ccdce25cfd837b99bea9ad92c4e511233199440488d217c92571e8", [: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", "5ce5f16b41208a50106afed3de6a2ed34f4acfd65715b82a0b84b49d995f95c1"}, "expo": {:hex, :expo, "1.0.0", "647639267e088717232f4d4451526e7a9de31a3402af7fcbda09b27e9a10395a", [:mix], [], "hexpm", "18d2093d344d97678e8a331ca0391e85d29816f9664a25653fd7e6166827827c"}, + "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, + "exvcr": {:hex, :exvcr, "0.15.1", "772db4d065f5136c6a984c302799a79e4ade3e52701c95425fa2229dd6426886", [:mix], [{:exactor, "~> 2.2", [hex: :exactor, repo: "hexpm", optional: false]}, {:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "de4fc18b1d672d9b72bc7468735e19779aa50ea963a1f859ef82cd9e294b13e3"}, "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, "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"}, "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, @@ -18,9 +21,11 @@ "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, "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.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "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.1", "c7f58c120b2b5aa5fd80d540a89fdf866ed42f1f3994e4fe189abebeab610839", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "8a89a1eeccc2d798d6ea15496a6e4870b75e014d1af514b1b71fa33134f57814"}, + "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, diff --git a/test/cassettes/features/resolve-all.json b/test/cassettes/features/resolve-all.json new file mode 100644 index 0000000..97ad0f0 --- /dev/null +++ b/test/cassettes/features/resolve-all.json @@ -0,0 +1,34 @@ +[ + { + "request": { + "options": { + "with_body": "true" + }, + "body": "{}", + "url": "https://demo-control.fly.dev/api/features/", + "headers": { + "Authorization": "***", + "Content-Type": "application/json", + "User-Agent": "Kickplan/0.1.0 (sdk-elixir)" + }, + "method": "post", + "request_body": "" + }, + "response": { + "binary": false, + "type": "ok", + "body": "[{\"value\":false,\"key\":\"upload-contacts\"},{\"value\":false,\"key\":\"ai-llm-generation\"},{\"value\":false,\"key\":\"fancy\"},{\"value\":\"{\\n \\\"Currency\\\": \\\"US Dollar (USD)\\\",\\n \\\"Allowed Payment Gateways\\\": [\\\"PayPal\\\", \\\"Stripe\\\", \\\"Square\\\"]\\n }\",\"key\":\"payment-configuration\"},{\"value\":0,\"key\":\"contact-limit\"}]", + "headers": { + "cache-control": "max-age=0, private, must-revalidate", + "content-length": "308", + "content-type": "application/json; charset=utf-8", + "date": "Thu, 22 Aug 2024 19:19:14 GMT", + "server": "Fly/5e55b43a7 (2024-08-21)", + "x-request-id": "F-4jJk91UgTYSXkAAAyB", + "via": "1.1 fly.io", + "fly-request-id": "01J5XREQJRTKHBC3Y5CJQAPDYK-sea" + }, + "status_code": 200 + } + } +] \ No newline at end of file diff --git a/test/cassettes/features/resolve.json b/test/cassettes/features/resolve.json new file mode 100644 index 0000000..9a5bfa7 --- /dev/null +++ b/test/cassettes/features/resolve.json @@ -0,0 +1,34 @@ +[ + { + "request": { + "options": { + "with_body": "true" + }, + "body": "{}", + "url": "https://demo-control.fly.dev/api/features/contact-limit", + "headers": { + "Authorization": "***", + "Content-Type": "application/json", + "User-Agent": "Kickplan/0.1.0 (sdk-elixir)" + }, + "method": "post", + "request_body": "" + }, + "response": { + "binary": false, + "type": "ok", + "body": "{\"value\":0,\"key\":\"contact-limit\"}", + "headers": { + "cache-control": "max-age=0, private, must-revalidate", + "content-length": "33", + "content-type": "application/json; charset=utf-8", + "date": "Thu, 22 Aug 2024 19:43:57 GMT", + "server": "Fly/5e55b43a7 (2024-08-21)", + "x-request-id": "F-4kf6KMXpujMxwAAAyx", + "via": "1.1 fly.io", + "fly-request-id": "01J5XSVZZEBDC5XTBGR0QKYXXJ-sea" + }, + "status_code": 200 + } + } +] \ No newline at end of file diff --git a/test/cassettes/metrics/flush.json b/test/cassettes/metrics/flush.json new file mode 100644 index 0000000..38666e4 --- /dev/null +++ b/test/cassettes/metrics/flush.json @@ -0,0 +1,33 @@ +[ + { + "request": { + "options": { + "with_body": "true" + }, + "body": "{}", + "url": "https://demo-control.fly.dev/api/metrics/flush", + "headers": { + "Authorization": "***", + "Content-Type": "application/json", + "User-Agent": "Kickplan/0.1.0 (sdk-elixir)" + }, + "method": "post", + "request_body": "" + }, + "response": { + "binary": false, + "type": "ok", + "body": "", + "headers": { + "cache-control": "max-age=0, private, must-revalidate", + "date": "Thu, 22 Aug 2024 22:37:26 GMT", + "server": "Fly/5e55b43a7 (2024-08-21)", + "x-request-id": "F-4t9xPC0bL6wOQAAA0B", + "via": "1.1 fly.io", + "fly-request-id": "01J5Y3SMMC67WZPQJYTVY1ZVP0-sea", + "content-length": "0" + }, + "status_code": 202 + } + } +] \ No newline at end of file diff --git a/test/cassettes/metrics/set.json b/test/cassettes/metrics/set.json new file mode 100644 index 0000000..885c1f1 --- /dev/null +++ b/test/cassettes/metrics/set.json @@ -0,0 +1,33 @@ +[ + { + "request": { + "options": { + "with_body": "true" + }, + "body": "{\"value\":3,\"time\":\"2024-08-22T21:40:19.923716Z\",\"key\":\"sdk_elixir_test\",\"account_key\":\"9a592f57-6da0-408e-99e7-8918b48a7dbe\",\"idempotency_key\":\"004b7f07-71fd-4ea5-a43c-dcde2516305b\"}", + "url": "https://demo-control.fly.dev/api/metrics/set", + "headers": { + "Authorization": "***", + "Content-Type": "application/json", + "User-Agent": "Kickplan/0.1.0 (sdk-elixir)" + }, + "method": "post", + "request_body": "" + }, + "response": { + "binary": false, + "type": "ok", + "body": "", + "headers": { + "cache-control": "max-age=0, private, must-revalidate", + "date": "Thu, 22 Aug 2024 21:40:19 GMT", + "server": "Fly/5e55b43a7 (2024-08-21)", + "x-request-id": "F-4q2U-SUX7DPj0AAAzx", + "via": "1.1 fly.io", + "fly-request-id": "01J5Y0H2HYJAJ2PJVFS2D0TM7A-sea", + "content-length": "0" + }, + "status_code": 202 + } + } +] \ No newline at end of file diff --git a/test/kickplan/resource/features_test.exs b/test/kickplan/resource/features_test.exs new file mode 100644 index 0000000..16b9cc1 --- /dev/null +++ b/test/kickplan/resource/features_test.exs @@ -0,0 +1,28 @@ +defmodule Kickplan.FeaturesTest do + use Kickplan.VCRCase, async: true + + alias Kickplan.Features + alias Kickplan.Schema.Resolution + + describe "resolve/2" do + test "success: resolves all features" do + use_cassette "features/resolve-all" do + {:ok, resolutions} = Features.resolve() + + assert length(resolutions) == 5 + + assert Enum.all?(resolutions, fn resolution -> + %Resolution{} = resolution + end) + end + end + + test "success: resolves a single feature" do + use_cassette "features/resolve" do + {:ok, resolution} = Features.resolve("contact-limit") + + assert %Resolution{key: "contact-limit"} = resolution + end + end + end +end diff --git a/test/kickplan/resource/metrics_test.exs b/test/kickplan/resource/metrics_test.exs new file mode 100644 index 0000000..8ef4d97 --- /dev/null +++ b/test/kickplan/resource/metrics_test.exs @@ -0,0 +1,33 @@ +defmodule Kickplan.MetricsTest do + use Kickplan.VCRCase, async: true + + alias Kickplan.Metrics + + describe "flush/0" do + test "success: flushes the metrics" do + use_cassette "metrics/flush" do + assert {:ok, true} = Metrics.flush() + end + end + end + + describe "set/1" do + test "success: resolves a single feature" do + use_cassette "metrics/set" do + assert {:ok, true} = + Metrics.set( + key: "sdk_elixir_test", + value: 3, + account_key: "9a592f57-6da0-408e-99e7-8918b48a7dbe", + time: DateTime.utc_now(), + idempotency_key: "004b7f07-71fd-4ea5-a43c-dcde2516305b" + ) + end + end + + test "failure: request has invalid params" do + assert {:error, %NimbleOptions.ValidationError{}} = + Metrics.set(key: "sdk_elixir_test") + end + end +end diff --git a/test/support/vcr_case.ex b/test/support/vcr_case.ex new file mode 100644 index 0000000..4a1fb20 --- /dev/null +++ b/test/support/vcr_case.ex @@ -0,0 +1,18 @@ +defmodule Kickplan.VCRCase do + @moduledoc false + + use ExUnit.CaseTemplate + + using do + quote do + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + end + end + + setup do + ExVCR.Config.cassette_library_dir("test/cassettes") + ExVCR.Config.filter_request_headers("Authorization") + + :ok + end +end From 4f6dd40c21d853b1b4903ed53d09fea26ff7d880 Mon Sep 17 00:00:00 2001 From: Jared Hoyt Date: Fri, 23 Aug 2024 11:31:52 -0700 Subject: [PATCH 2/3] [fixup] mix check --- lib/kickplan/requests/metrics/set.ex | 1 + lib/kickplan/resource/metrics.ex | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/lib/kickplan/requests/metrics/set.ex b/lib/kickplan/requests/metrics/set.ex index db37740..723fed8 100644 --- a/lib/kickplan/requests/metrics/set.ex +++ b/lib/kickplan/requests/metrics/set.ex @@ -17,5 +17,6 @@ defmodule Kickplan.Requests.Metrics.Set do time: [type: :any] ) + @doc false def options, do: @options end diff --git a/lib/kickplan/resource/metrics.ex b/lib/kickplan/resource/metrics.ex index 08d5abb..05e91cd 100644 --- a/lib/kickplan/resource/metrics.ex +++ b/lib/kickplan/resource/metrics.ex @@ -5,12 +5,18 @@ defmodule Kickplan.Metrics do alias Kickplan.{Client, Requests} + @doc """ + TODO + """ def flush do with {:ok, resp} <- Client.post("metrics/flush") do {:ok, resp.success?} end end + @doc """ + TODO + """ def set(opts \\ %{}) do with {:ok, params} <- validate(opts, Requests.Metrics.Set), {:ok, resp} <- Client.post("metrics/set", params) do From 35bc9184bbf7c9e10e32778e1c2cece64662ccde Mon Sep 17 00:00:00 2001 From: Jared Hoyt Date: Fri, 23 Aug 2024 11:54:20 -0700 Subject: [PATCH 3/3] [fixup] add base url to config for test --- config/config.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.exs b/config/config.exs index 705540e..db5824f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,5 +1,7 @@ import Config +config :kickplan, :base_url, "https://demo-control.fly.dev/api" + if File.exists?("config/config.secret.exs") do import_config "config.secret.exs" end