From 1de230d944916150b971ea55bf9a295aaae8d624 Mon Sep 17 00:00:00 2001 From: Michael Klishin Date: Sat, 3 Aug 2024 05:00:11 -0400 Subject: [PATCH] New data type: tagged string In some cases the string data type is not expressive enough. It requires key-specific handling in the application, that is, the key name becomes hardcoded if the string has any special meaning to the app. By introducing a new data type we enforce a convention where a schema can define a "special" kind (in fact, multiple kinds) of string and it will be expressed in the generated value, which will be a tagged tuple. This approach avoids parser extensions, whether this means native syntax or a complex data type extension such as the duration data type. Closes #40. --- src/cuttlefish_conf.erl | 1 + src/cuttlefish_datatypes.erl | 24 +++++++++++++++++++++++- src/cuttlefish_escript.erl | 2 +- src/cuttlefish_generator.erl | 3 ++- test/cuttlefish_integration_tests.erl | 7 +++++++ test/tagged_string.schema | 3 +++ 6 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 test/tagged_string.schema diff --git a/src/cuttlefish_conf.erl b/src/cuttlefish_conf.erl index ccf2a6b..222990c 100644 --- a/src/cuttlefish_conf.erl +++ b/src/cuttlefish_conf.erl @@ -233,6 +233,7 @@ pretty_datatype({duration, _}) -> "a time duration with units, e.g. '10s' for 10 pretty_datatype(bytesize) -> "a byte size with units, e.g. 10GB"; pretty_datatype({integer, I}) -> "the integer " ++ integer_to_list(I); pretty_datatype({string, S}) -> "the text \"" ++ S ++ "\""; +pretty_datatype({tagged_string, {Tag, String}}) -> "the text \"" ++ String ++ "\"" ++ " tagged as \"" ++ Tag ++ "\""; pretty_datatype({atom, A}) -> "the text \"" ++ atom_to_list(A) ++ "\""; pretty_datatype({ip, {IP, Port}}) -> ?FMT("the address ~ts:~tp", [IP, Port]); pretty_datatype({domain_socket, {local, Path, Port}}) -> diff --git a/src/cuttlefish_datatypes.erl b/src/cuttlefish_datatypes.erl index 469006b..8bcf10b 100644 --- a/src/cuttlefish_datatypes.erl +++ b/src/cuttlefish_datatypes.erl @@ -42,6 +42,7 @@ {percent, integer} | {percent, float} | float | + tagged_string | {list, datatype()}. -type extended() :: { integer, integer() } | { string, string() } | @@ -54,7 +55,8 @@ { bytesize, string() } | { {percent, integer}, integer() } | { {percent, float}, float() } | - { float, float() }. + { float, float() } | + { tagged_string, string() }. -type datatype_list() :: [ datatype() | extended() ]. -export_type([datatype/0, extended/0, datatype_list/0]). @@ -92,6 +94,7 @@ is_supported(bytesize) -> true; is_supported({percent, integer}) -> true; is_supported({percent, float}) -> true; is_supported(float) -> true; +is_supported(tagged_string) -> true; is_supported({list, {list, _}}) -> % lists of lists are not supported false; @@ -102,6 +105,7 @@ is_supported(_) -> false. -spec is_extended(any()) -> boolean(). is_extended({integer, I}) when is_integer(I) -> true; is_extended({string, S}) when is_list(S) -> true; +is_extended({tagged_string, S}) when is_list(S) -> true; is_extended({atom, A}) when is_atom(A) -> true; is_extended({file, F}) when is_list(F) -> true; is_extended({directory, D}) when is_list(D) -> true; @@ -127,6 +131,7 @@ is_extended(_) -> false. -spec extended_from(extended()) -> datatype(). extended_from({integer, _}) -> integer; extended_from({string, _}) -> string; +extended_from({tagged_string, _}) -> tagged_string; extended_from({atom, _}) -> atom; extended_from({file, _}) -> file; extended_from({directory, _}) -> directory; @@ -184,6 +189,8 @@ to_string(Bytesize, bytesize) when is_integer(Bytesize) -> cuttlefish_bytesize:t to_string(String, string) when is_list(String) -> String; +to_string({Tag, String}, tagged_string) when is_list(Tag), is_list(String) -> Tag ++ ":" ++ String; + to_string(File, file) when is_list(File) -> File; to_string(Directory, directory) when is_list(Directory) -> Directory; @@ -239,6 +246,9 @@ from_string({FQDN, Port}, fqdn) when is_list(FQDN), is_integer(Port) -> {FQDN, P from_string(String, fqdn) when is_list(String) -> from_string_to_fqdn(String, lists:split(string:rchr(String, $:), String)); +from_string(String, tagged_string) when is_list(String) -> + from_string_to_tagged_string(String, lists:split(string:rchr(String, $:), String)); + from_string({local, UDS, Port}, domain_socket) when is_list(UDS), is_integer(Port) -> {local, UDS, Port}; from_string(String, domain_socket) when is_list(String) -> from_string_to_uds(String, lists:split(string:rchr(String, $:), String)); @@ -363,6 +373,14 @@ from_string_to_fqdn(String, {FQDNPlusColon, PortString}) -> FQDN = droplast(FQDNPlusColon), fqdn_conversions(String, FQDN, validate_fqdn(FQDN), port_to_integer(PortString)). +from_string_to_tagged_string(String, {[], String}) -> + %% does not follow the tag:value format convention + {error, {conversion, {String, "tagged string"}}}; +from_string_to_tagged_string(_String, {TagPlusColon, TaggedValue}) -> + %% Drop the trailing colon from the tag + Tag = droplast(TagPlusColon), + {list_to_atom(Tag), TaggedValue}. + from_string_to_uds(String, {[], String}) -> {error, {conversion, {String, 'UDS'}}}; from_string_to_uds(String, {UDSPlusColon, PortString}) -> @@ -642,6 +660,7 @@ is_supported_test() -> ?assert(is_supported({duration, ms})), ?assert(is_supported(bytesize)), ?assert(is_supported(domain_socket)), + ?assert(is_supported(tagged_string)), ?assert(is_supported({list, string})), ?assert(not(is_supported({list, {list, string}}))), ?assert(not(is_supported(some_unsupported_type))), @@ -691,6 +710,9 @@ is_extended_test() -> ?assertEqual(true, is_extended({{percent, float}, "10%"})), ?assertEqual(true, is_extended({{percent, float}, 0.1})), ?assertEqual(true, is_extended({float, 0.1})), + + ?assertEqual(true, is_extended({tagged_string, "tag:value"})), + ok. -endif. diff --git a/src/cuttlefish_escript.erl b/src/cuttlefish_escript.erl index 6c12ed3..9a01a56 100644 --- a/src/cuttlefish_escript.erl +++ b/src/cuttlefish_escript.erl @@ -77,7 +77,7 @@ parse_and_command(Args) -> main(Args) -> {Command, ParsedArgs, Extra} = parse_and_command(Args), - SuggestedLogLevel = list_to_atom(proplists:get_value(log_level, ParsedArgs)), + SuggestedLogLevel = list_to_atom(proplists:get_value(log_level, ParsedArgs, "notice")), LogLevel = case lists:member(SuggestedLogLevel, [debug, info, notice, warning, error, critical, alert, emergency]) of true -> SuggestedLogLevel; _ -> notice diff --git a/src/cuttlefish_generator.erl b/src/cuttlefish_generator.erl index 379c03c..32f4e4a 100644 --- a/src/cuttlefish_generator.erl +++ b/src/cuttlefish_generator.erl @@ -559,7 +559,8 @@ transform_supported_type(DT, Value) -> {error, Message} -> {error, Message}; NewValue -> {ok, NewValue} catch - Class:Error -> + Class:Error:_Stacktrace -> + %% io:format("Failed to transform a type. Stacktrace: ~p~n", [Stacktrace]), {error, {transform_type_exception, {DT, {Class, Error}}}} end. diff --git a/test/cuttlefish_integration_tests.erl b/test/cuttlefish_integration_tests.erl index 6d02113..65d608b 100644 --- a/test/cuttlefish_integration_tests.erl +++ b/test/cuttlefish_integration_tests.erl @@ -174,6 +174,13 @@ duration_test() -> ErrConfig = cuttlefish_generator:map(Schema, Conf2), ?assertMatch({error, transform_datatypes, _}, ErrConfig). +tagged_string_test() -> + Schema = cuttlefish_schema:files(["test/tagged_string.schema"]), + + Conf = conf_parse:parse(<<"tagged_key = tagged:e614d97599dab483f\n">>), + NewConfig = cuttlefish_generator:map(Schema, Conf), + ?assertEqual({tagged, "e614d97599dab483f"}, proplists:get_value(tagged_key, proplists:get_value(cuttlefish, NewConfig))). + proplist_equals(Expected, Actual) -> ExpectedKeys = lists:sort(proplists:get_keys(Expected)), ActualKeys = lists:sort(proplists:get_keys(Actual)), diff --git a/test/tagged_string.schema b/test/tagged_string.schema new file mode 100644 index 0000000..5a9d9b2 --- /dev/null +++ b/test/tagged_string.schema @@ -0,0 +1,3 @@ +{mapping, "tagged_key", "cuttlefish.tagged_key", [ + {datatype, [tagged_string]} +]}.