Skip to content

Commit c290599

Browse files
kaifee-haqueLuis Beligante
andauthored
fix: Allow Elixir projects to read flags from a local file (#106)
**Requirements** - [x] I have added test coverage for new or changed functionality - [x] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) - [ ] I have validated my changes against all supported platform versions **Context** While trying to configure our Elixir service to read feature flags from a local file (for local development purposes), we ran into an issue where the server SDK would throw an "unsupported extension" error, even though the configured path pointed to a YAML file. **Root cause** Erlang uses charlists to represent strings, while Elixir uses binaries. So when we pass a path to a YAML file to the `ldclient` config in our Elixir project, the Erlang function that parses the file extension on the SDK side receives a binary, which it does not support. Thus, the SDK throws an error claiming that we're using an invalid file type. **Describe the solution you've provided** This PR adds a helper function that handles binary type strings. If the `FilePath` is a binary, we simply convert it to a charlist. --------- Co-authored-by: Luis Beligante <luis.beligante@thescore.com>
1 parent b709a19 commit c290599

File tree

4 files changed

+33
-12
lines changed

4 files changed

+33
-12
lines changed

src/ldclient_config.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
use_ldd => boolean(),
7676
send_events => boolean(),
7777
file_datasource => boolean(),
78-
file_paths => [string()],
78+
file_paths => [string() | binary()],
7979
file_auto_update => boolean(),
8080
file_poll_interval => pos_integer(),
8181
file_allow_duplicate_keys => boolean(),

src/ldclient_instance.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
cache_ttl => integer(),
3737
send_events => boolean(),
3838
file_datasource => boolean(),
39-
file_paths => [string()],
39+
file_paths => [string() | binary()],
4040
file_auto_update => boolean(),
4141
file_poll_interval => pos_integer(),
4242
file_allow_duplicate_keys => boolean(),

src/ldclient_update_file_server.erl

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
-export([code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
1616

1717
-type state() :: #{
18-
file_paths := [string()],
18+
file_paths := [string() | binary()],
1919
file_auto_update := boolean(),
2020
file_poll_interval := pos_integer(),
2121
file_allow_duplicate_keys := boolean(),
@@ -155,7 +155,7 @@ upsert_valid_flag_data(true, Tag, ParsedFlags, ParsedSegments) ->
155155
upsert_valid_flag_data(false, _Tag, _ParsedFlags, _ParsedSegments) ->
156156
error.
157157

158-
-spec read_file(FilePath :: string(), Extension :: string()) -> {ok | error, map()}.
158+
-spec read_file(FilePath :: string() | binary(), Extension :: string()) -> {ok | error, map()}.
159159
read_file(FilePath, ".yaml") ->
160160
Result = yamerl_constr:file(FilePath, [{detailed_constr, true}]),
161161
[Head | _] = ldclient_yaml_mapper:to_map_docs(Result, []),
@@ -167,7 +167,7 @@ read_file(FilePath, _Extension) ->
167167
error_logger:warning_msg("File had unrecognized file extension. Valid extensions are .yaml and .json. File: ~p", [FilePath]),
168168
{error, #{}}.
169169

170-
-spec try_read_file(FilePath :: string(), Extension :: string()) -> {ok | error, map()}.
170+
-spec try_read_file(FilePath :: string() | binary(), Extension :: string()) -> {ok | error, map()}.
171171
try_read_file(FilePath, Extension) ->
172172
try
173173
read_file(FilePath, Extension)
@@ -176,7 +176,7 @@ try_read_file(FilePath, Extension) ->
176176
{error, #{}}
177177
end.
178178

179-
-spec load_file(FilePath :: string(), Extension :: string(), State :: state(), LoadedFlags :: map(), LoadedSegments :: map(), FlagCount :: non_neg_integer()) ->
179+
-spec load_file(FilePath :: string() | binary(), Extension :: string(), State :: state(), LoadedFlags :: map(), LoadedSegments :: map(), FlagCount :: non_neg_integer()) ->
180180
{ok | error, NewState :: state(), LoadedFlags :: map(), LoadedSegments :: map(), FlagCount :: non_neg_integer()}.
181181
load_file(FilePath, Extension, #{file_info := CurFileInfo} = State, LoadedFlags, LoadedSegments, FlagCount) ->
182182
ModifiedTime = filelib:last_modified(FilePath),
@@ -196,7 +196,7 @@ process_decoded_file(ok, Decoded, State, LoadedFlags, LoadedSegments, FlagCount)
196196
process_decoded_file(error, _Decoded, State, LoadedFlags, LoadedSegments, FlagCount) ->
197197
{error, State, LoadedFlags, LoadedSegments, FlagCount}.
198198

199-
-spec check_modified(FilesToCheck :: [string()], Modified :: boolean(), State :: state()) ->
199+
-spec check_modified(FilesToCheck :: [string() | binary()], Modified :: boolean(), State :: state()) ->
200200
{Modified :: boolean(), State :: state()}.
201201
check_modified([], Modified, State) ->
202202
{Modified, State};
@@ -207,29 +207,29 @@ check_modified([FileToCheck | RestOfFiles], Modified, State) ->
207207
NewModified = Modified or (ExistingModifiedTime =/= ModifiedTime),
208208
check_modified(RestOfFiles, NewModified, State).
209209

210-
-spec load_files_if_modified(Files :: [string()], State :: state()) -> {ok | error, State :: state()}.
210+
-spec load_files_if_modified(Files :: [string() | binary()], State :: state()) -> {ok | error, State :: state()}.
211211
load_files_if_modified(Files, State) ->
212212
case check_modified(Files, false, State) of
213213
{true, UpdatedState} -> load_files(Files, UpdatedState);
214214
{false, UpdatedState} -> {ok, UpdatedState}
215215
end.
216216

217-
-spec load_regular_file(FilePath :: string(), IsRegularFile :: boolean(), State :: state(), LoadedFlags :: map(), LoadedSegments :: map(), FlagCount :: non_neg_integer()) ->
217+
-spec load_regular_file(FilePath :: string() | binary(), IsRegularFile :: boolean(), State :: state(), LoadedFlags :: map(), LoadedSegments :: map(), FlagCount :: non_neg_integer()) ->
218218
{ok | error, NewState :: state(), LoadedFlags :: map(), LoadedSegments :: map(), FlagCount :: non_neg_integer()}.
219219
load_regular_file(FileToLoad, true, State, LoadedFlags, LoadedSegments, FlagCount) ->
220220
load_file(FileToLoad, filename:extension(FileToLoad), State, LoadedFlags, LoadedSegments, FlagCount);
221221
load_regular_file(_FileToLoad, false, State, LoadedFlags, LoadedSegments, FlagCount) ->
222222
{error, State, LoadedFlags, LoadedSegments, FlagCount}.
223223

224-
-spec load_files(Files :: [string()], State :: state()) -> {ok | error, State :: state()}.
224+
-spec load_files(Files :: [string() | binary()], State :: state()) -> {ok | error, State :: state()}.
225225
load_files(Files, State) ->
226226
load_files(Files, State, #{}, #{}, 0, ok).
227227

228-
-spec load_files(Files :: [string()], State :: state(), CombinedFlags :: map(), CombinedSegments :: map(), FlagCount :: non_neg_integer(), Status :: ok | error) ->
228+
-spec load_files(Files :: [string() | binary()], State :: state(), CombinedFlags :: map(), CombinedSegments :: map(), FlagCount :: non_neg_integer(), Status :: ok | error) ->
229229
{ok | error, State :: state()}.
230230
load_files([FileToLoad | RestOfFiles], State, LoadedFlags, LoadedSegments, FlagCount, ok) ->
231231
{NewStatus, NewState, CombinedFlags, CombinedSegments, UpdatedCount} =
232-
load_regular_file(FileToLoad, filelib:is_regular(FileToLoad), State, LoadedFlags, LoadedSegments, FlagCount),
232+
load_regular_file(handle_file_path_type(FileToLoad), filelib:is_regular(handle_file_path_type(FileToLoad)), State, LoadedFlags, LoadedSegments, FlagCount),
233233
load_files(RestOfFiles, NewState, CombinedFlags, CombinedSegments, UpdatedCount, NewStatus);
234234
load_files([], #{file_allow_duplicate_keys := AllowDuplicateKeys, storage_tag := Tag} = State, LoadedFlags, LoadedSegments, FlagCount, ok) ->
235235
Valid = (FlagCount == length(maps:keys(LoadedFlags))) or AllowDuplicateKeys,
@@ -238,3 +238,9 @@ load_files([], #{file_allow_duplicate_keys := AllowDuplicateKeys, storage_tag :=
238238
load_files(_Files, State, _LoadedFlags, _LoadedSegments, _FlagCount, error) ->
239239
%% If there is an error, then do not process any more files.
240240
{error, State}.
241+
242+
-spec handle_file_path_type(FilePath :: string() | binary()) -> string().
243+
handle_file_path_type(FilePath) when is_binary(FilePath) ->
244+
binary_to_list(FilePath);
245+
handle_file_path_type(FilePath) ->
246+
FilePath.

test/ldclient_file_SUITE.erl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
check_file_simple_flags_yaml/1,
1919
check_file_all_properties_yaml/1,
2020
check_multiple_data_files/1,
21+
check_binary_file_path/1,
2122
check_file_watcher/1,
2223
check_with_missing_file/1,
2324
check_with_only_missing_file/1,
@@ -35,6 +36,7 @@ all() ->
3536
check_file_simple_flags_yaml,
3637
check_file_all_properties_yaml,
3738
check_multiple_data_files,
39+
check_binary_file_path,
3840
check_file_watcher,
3941
check_with_missing_file,
4042
check_with_only_missing_file,
@@ -91,6 +93,14 @@ init_per_suite(Config) ->
9193
},
9294
ldclient:start_instance("", simple_flags_yaml, OptionsSimpleFlagsYaml),
9395

96+
OptionsBinaryPathYaml = #{
97+
datasource => file,
98+
send_events => false,
99+
file_paths => [list_to_binary(DataFileSimpleFlagsYaml)],
100+
feature_store => ldclient_storage_map
101+
},
102+
ldclient:start_instance("", binary_path_yaml, OptionsBinaryPathYaml),
103+
94104
DataFileAllPropertiesYaml = code:priv_dir(ldclient) ++ "/flags-all-properties.yaml",
95105
OptionsAllPropertiesYaml = #{
96106
datasource => file,
@@ -208,6 +218,11 @@ check_multiple_data_files(_) ->
208218
{{0, true, fallthrough}, _} = ldclient_eval:flag_key_for_context(all_properties_yaml, <<"my-boolean-flag-key">>, #{key => <<"user123">>, kind => <<"user">>}, "foo"),
209219
{{0, 3, fallthrough}, _} = ldclient_eval:flag_key_for_context(all_properties_yaml, <<"my-integer-flag-key">>, #{key => <<"user123">>, kind => <<"user">>}, "foo").
210220

221+
check_binary_file_path(_) ->
222+
{{0, <<"value-1">>, fallthrough}, _} = ldclient_eval:flag_key_for_context(binary_path_yaml, <<"my-string-flag-key">>, #{key => <<"user123">>, kind => <<"user">>}, "foo"),
223+
{{0, true, fallthrough}, _} = ldclient_eval:flag_key_for_context(binary_path_yaml, <<"my-boolean-flag-key">>, #{key => <<"user123">>, kind => <<"user">>}, "foo"),
224+
{{0, 3, fallthrough}, _} = ldclient_eval:flag_key_for_context(binary_path_yaml, <<"my-integer-flag-key">>, #{key => <<"user123">>, kind => <<"user">>}, "foo").
225+
211226
check_file_watcher(_) ->
212227
{{0, true, fallthrough}, _} = ldclient_eval:flag_key_for_context(watch_files, <<"test-flag">>, #{key => <<"user123">>, kind => <<"user">>}, "foo"),
213228
%The timestamp for the modified time is in seconds. So we wait a moment to get a new timestamp compared to creation.

0 commit comments

Comments
 (0)