Skip to content

Commit 11e53ea

Browse files
committed
add provider
1 parent 8bfd768 commit 11e53ea

9 files changed

+176
-50
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,139 @@
11
defmodule ElixirProvider do
2-
alias OpenFeature.EvaluationDetails
3-
alias ElixirProvider.ResponseFlagEvaluation
4-
alias ElixirProvider.GoFeatureFlagMetadata
5-
alias ElixirProvider.ContextTransformer
6-
alias ElixirProvider.RequestFlagEvaluation
2+
@behaviour OpenFeature.Provider
3+
4+
alias OpenFeature.ResolutionDetails
75
alias ElixirProvider.GoFeatureFlagOptions
8-
alias ElixirProvider.Types
6+
alias ElixirProvider.HttpClient
7+
alias ElixirProvider.DataCollectorHook
98
alias ElixirProvider.CacheController
9+
alias ElixirProvider.ResponseFlagEvaluation
1010
alias ElixirProvider.GoFWebSocketClient
11-
alias ElixirProvider.HttpClient
11+
alias ElixirProvider.RequestFlagEvaluation
12+
alias ElixirProvider.ContextTransformer
13+
alias ElixirProvider.GofEvaluationContext
1214

1315
@moduledoc """
14-
The provider for GO Feature Flag, managing HTTP requests, caching, and flag evaluation.
16+
The GO Feature Flag provider for OpenFeature, managing HTTP requests, caching, and flag evaluation.
1517
"""
1618

1719
defstruct [
1820
:options,
19-
:_http_client,
20-
_data_collector_hook: nil,
21-
_ws: nil,
21+
:http_client,
22+
:data_collector_hook,
23+
:ws,
24+
:domain
2225
]
2326

2427
@type t :: %__MODULE__{
25-
options: GoFeatureFlagOptions.t(),
26-
_http_client: HttpClient.t(),
27-
_data_collector_hook: any(),
28-
_ws: GoFWebSocketClient.t(),
29-
}
28+
options: GoFeatureFlagOptions.t(),
29+
http_client: HttpClient.t(),
30+
data_collector_hook: DataCollectorHook.t() | nil,
31+
ws: GoFWebSocketClient.t(),
32+
domain: String.t()
33+
}
34+
35+
@impl true
36+
def initialize(%__MODULE__{} = provider, domain, _context) do
37+
{:ok, http_client} = HttpClient.start_http_connection(provider.options)
38+
CacheController.start_link(provider.options)
39+
{:ok, data_collector_hook} = DataCollectorHook.start_link(provider.options, http_client)
40+
{:ok, ws} = GoFWebSocketClient.start_link(provider.options.endpoint)
41+
42+
updated_provider = %__MODULE__{
43+
provider
44+
| domain: domain,
45+
http_client: http_client,
46+
data_collector_hook: data_collector_hook,
47+
ws: ws
48+
}
49+
50+
{:ok, updated_provider}
51+
end
52+
53+
@impl true
54+
def shutdown(%__MODULE__{ws: ws} = provider) do
55+
Process.exit(ws, :normal)
56+
CacheController.clear()
57+
if provider.data_collector_hook, do: DataCollectorHook.shutdown(provider.data_collector_hook)
58+
:ok
59+
end
60+
61+
@impl true
62+
def resolve_boolean_value(provider, key, default, context) do
63+
generic_resolve(provider, :boolean, key, default, context)
64+
end
65+
66+
@impl true
67+
def resolve_string_value(provider, key, default, context) do
68+
generic_resolve(provider, :string, key, default, context)
69+
end
70+
71+
@impl true
72+
def resolve_number_value(provider, key, default, context) do
73+
generic_resolve(provider, :number, key, default, context)
74+
end
75+
76+
@impl true
77+
def resolve_map_value(provider, key, default, context) do
78+
generic_resolve(provider, :map, key, default, context)
79+
end
80+
81+
defp generic_resolve(provider, type, flag_key, default_value, context) do
82+
{:ok, goff_context} = ContextTransformer.transform_context(context)
83+
goff_request = %RequestFlagEvaluation{user: goff_context, default_value: default_value}
84+
eval_context_hash = GofEvaluationContext.hash(goff_context)
85+
86+
response_body =
87+
case CacheController.get(flag_key, eval_context_hash) do
88+
{:ok, cached_response} ->
89+
cached_response
90+
91+
:miss ->
92+
# Fetch from HTTP if cache miss
93+
case HttpClient.post(provider.http_client, "/v1/feature/#{flag_key}/eval", goff_request) do
94+
{:ok, response} -> handle_response(flag_key, eval_context_hash, response)
95+
{:error, reason} -> {:error, {:unexpected_error, reason}}
96+
end
97+
end
98+
99+
handle_flag_resolution(response_body, type, flag_key, default_value)
100+
end
101+
102+
defp handle_response(flag_key, eval_context_hash, response) do
103+
# Build the flag evaluation struct directly from the response map
104+
flag_eval = ResponseFlagEvaluation.decode(response)
105+
106+
# Cache the response if it's marked as cacheable
107+
if flag_eval.cacheable do
108+
CacheController.set(flag_key, eval_context_hash, response)
109+
end
110+
111+
{:ok, flag_eval}
112+
end
113+
114+
defp handle_flag_resolution(response, type, flag_key, _default_value) do
115+
case response do
116+
{:ok, %ResponseFlagEvaluation{value: value, reason: reason}} ->
117+
case {type, value} do
118+
{:boolean, val} when is_boolean(val) ->
119+
{:ok, %ResolutionDetails{value: val, reason: reason}}
120+
121+
{:string, val} when is_binary(val) ->
122+
{:ok, %ResolutionDetails{value: val, reason: reason}}
123+
124+
{:number, val} when is_number(val) ->
125+
{:ok, %ResolutionDetails{value: val, reason: reason}}
126+
127+
{:map, val} when is_map(val) ->
128+
{:ok, %ResolutionDetails{value: val, reason: reason}}
129+
130+
_ ->
131+
{:error, {:variant_not_found, "Expected #{type} but got #{inspect(value)} for flag #{flag_key}"}}
132+
end
30133

134+
_ ->
135+
{:error, {:flag_not_found, "Flag #{flag_key} not found"}}
136+
end
137+
end
31138

32139
end
Original file line numberDiff line numberDiff line change
@@ -1,16 +0,0 @@
1-
defmodule OpenFeature.Application do
2-
@moduledoc false
3-
4-
use Application
5-
6-
@impl true
7-
def start(_type, _args) do
8-
children = [
9-
OpenFeature.Store,
10-
OpenFeature.EventEmitter
11-
]
12-
13-
opts = [strategy: :one_for_one, name: OpenFeature.Supervisor]
14-
Supervisor.start_link(children, opts)
15-
end
16-
end

openfeature/providers/elixir-provider/lib/provider/context_transformer.ex

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ defmodule ElixirProvider.ContextTransformer do
22
@moduledoc """
33
Converts an OpenFeature EvaluationContext into a GO Feature Flag context.
44
"""
5-
alias ElixirProvider.EvaluationContext
5+
alias ElixirProvider.GofEvaluationContext
66
alias OpenFeature.Types
77

88
@doc """
@@ -16,14 +16,14 @@ defmodule ElixirProvider.ContextTransformer do
1616
end
1717

1818
@doc """
19-
Converts an EvaluationContext map into a ElixirProvider.EvaluationContext struct.
19+
Converts an EvaluationContext map into a ElixirProvider.GofEvaluationContext struct.
2020
Returns `{:ok, context}` on success, or `{:error, reason}` on failure.
2121
"""
22-
@spec transform_context(Types.context()) :: {:ok, EvaluationContext.t()} | {:error, String.t()}
22+
@spec transform_context(Types.context()) :: {:ok, GofEvaluationContext.t()} | {:error, String.t()}
2323
def transform_context(ctx) do
2424
case get_any_value(ctx) do
2525
{:ok, {key, value}} ->
26-
{:ok, %EvaluationContext{
26+
{:ok, %GofEvaluationContext{
2727
key: key,
2828
custom: value
2929
}}

openfeature/providers/elixir-provider/lib/provider/data_collector_hook.ex

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule DataCollectorHook do
1+
defmodule ElixirProvider.DataCollectorHook do
22
use GenServer
33
require Logger
44

@@ -25,12 +25,32 @@ defmodule DataCollectorHook do
2525
}
2626

2727
# Starts the GenServer and initializes with options
28-
def start_link(state) do
29-
GenServer.start_link(__MODULE__, state, name: __MODULE__)
28+
def start_link(options, http_client) do
29+
GenServer.start_link(__MODULE__, {options, http_client: http_client}, name: __MODULE__)
30+
end
31+
32+
def shutdown(state) do
33+
GenServer.stop(__MODULE__)
34+
collect_data(state.data_flush_interval)
35+
%__MODULE__{
36+
http_client: state.http_client,
37+
data_collector_endpoint: state.data_collector_endpoint,
38+
disable_data_collection: state.disable_data_collection,
39+
data_flush_interval: state.data_flush_interval,
40+
event_queue: []
41+
}
3042
end
3143

3244
# Initializes GenServer state and schedules the first flush
33-
def init(state) do
45+
def init(args) do
46+
state = %__MODULE__{
47+
http_client: args.http_client,
48+
data_collector_endpoint: args.options.endpoint,
49+
disable_data_collection: args.options.disable_data_collection || false,
50+
data_flush_interval: args.options.data_flush_interval || 60_000,
51+
event_queue: []
52+
}
53+
3454
schedule_collect_data(state.data_flush_interval)
3555
{:ok, state}
3656
end
@@ -40,6 +60,8 @@ defmodule DataCollectorHook do
4060
Process.send_after(self(), :collect_data, interval)
4161
end
4262

63+
### Hook Implementations
64+
4365
def after_hook(hook, hook_context, flag_evaluation_details, _hints) do
4466
if hook.disable_data_collection or flag_evaluation_details.reason != :CACHED do
4567
:ok
@@ -103,9 +125,8 @@ defmodule DataCollectorHook do
103125
meta: %{"provider" => "open-feature-elixir-sdk"},
104126
events: event_queue
105127
}
106-
|> Jason.encode!()
107128

108-
case http_client.post(http_client, endpoint, body) do
129+
case HttpClient.post(http_client, endpoint, body) do
109130
{:ok, response} ->
110131
Logger.info("Data sent successfully: #{inspect(response)}")
111132
:ok

openfeature/providers/elixir-provider/lib/provider/evaluation_context.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
defmodule ElixirProvider.EvaluationContext do
1+
defmodule ElixirProvider.GofEvaluationContext do
22
@moduledoc """
33
GoFeatureFlagEvaluationContext is an object representing a user context for evaluation.
44
"""

openfeature/providers/elixir-provider/lib/provider/flag_options.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ defmodule ElixirProvider.GoFeatureFlagOptions do
1515
endpoint: String.t(),
1616
cache_size: integer() | nil,
1717
data_flush_interval: integer() | nil,
18-
disable_data_collection: integer() | nil,
18+
disable_data_collection: boolean(),
1919
reconnect_interval: integer() | nil,
2020
disable_cache_invalidation: boolean() | nil
2121
}

openfeature/providers/elixir-provider/lib/provider/http_client.ex

+3-4
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ defmodule ElixirProvider.HttpClient do
1212
headers: list()
1313
}
1414

15-
@spec start_http_connection(client :: t()) :: {:ok, t()} | {:error, any()}
16-
def start_http_connection(client) do
17-
uri = URI.parse(client.endpoint)
15+
def start_http_connection(options) do
16+
uri = URI.parse(options.endpoint)
1817
scheme = if uri.scheme == "https", do: :https, else: :http
1918

2019
case Mint.HTTP.connect(scheme, uri.host, uri.port) do
2120
{:ok, conn} ->
2221
# Create the struct with the connection, endpoint, and default headers
2322
config = %__MODULE__{
2423
conn: conn,
25-
endpoint: client.endpoint,
24+
endpoint: options.endpoint,
2625
headers: [{"content-type", "application/json"}]
2726
}
2827

openfeature/providers/elixir-provider/lib/provider/request_flag_evaluation.ex

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ defmodule ElixirProvider.RequestFlagEvaluation do
22
@moduledoc """
33
RequestFlagEvaluation is an object representing a user context for evaluation.
44
"""
5-
alias ElixirProvider.EvaluationContext
5+
alias ElixirProvider.GofEvaluationContext
66

77
@enforce_keys [:user]
88
defstruct [:default_value, :user]
99

1010
@type t :: %__MODULE__{
11-
user: EvaluationContext.t(),
11+
user: GofEvaluationContext.t(),
1212
default_value: any()
1313
}
1414
end

openfeature/providers/elixir-provider/lib/provider/response_flag_evalution.ex

+15
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,19 @@ defmodule ElixirProvider.ResponseFlagEvaluation do
2626
metadata: map() | nil,
2727
cacheable: boolean() | nil
2828
}
29+
30+
@spec decode(map()) :: t()
31+
def decode(response) when is_map(response) do
32+
%__MODULE__{
33+
failed: response["failed"] || false,
34+
value: response["value"],
35+
variation_type: response["variationType"],
36+
reason: response["reason"] || "",
37+
error_code: response["errorCode"],
38+
metadata: response["metadata"] || %{},
39+
cacheable: Map.get(response, "cacheable", false),
40+
track_events: response["track_events"],
41+
version: response["version"]
42+
}
43+
end
2944
end

0 commit comments

Comments
 (0)