%% ------------------------------------------------------------------- %% %% cuttlefish_datatypes: handles datatypes in cuttlefish schemas %% %% Copyright (c) 2013 Basho Technologies, Inc. All Rights Reserved. %% %% This file is provided to you under the Apache License, %% Version 2.0 (the "License"); you may not use this file %% except in compliance with the License. You may obtain %% a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, %% software distributed under the License is distributed on an %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY %% KIND, either express or implied. See the License for the %% specific language governing permissions and limitations %% under the License. %% %% ------------------------------------------------------------------- -module(cuttlefish_datatypes). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -compile(export_all). -endif. -type datatype() :: integer | string | atom | file | directory | flag | {flag, atom(), atom()} | {flag, {atom(), term()}, {atom(), term()}} | {enum, [atom()]} | ip | {duration, cuttlefish_duration:time_unit() } | bytesize | {percent, integer} | {percent, float} | float. -type extended() :: { integer, integer() } | { string, string() } | { file, file:filename() } | { directory, file:filename() } | { atom, atom() } | { ip, { string(), integer() } } | { {duration, cuttlefish_duration:time_unit() }, string() } | { bytesize, string() } | { {percent, integer}, integer() } | { {percent, float}, float() } | { float, float() }. -type datatype_list() :: [ datatype() | extended() ]. -export_type([datatype/0, extended/0, datatype_list/0]). -export([ is_supported/1, is_extended/1, is_valid_list/1, from_string/2, to_string/2, extended_from/1 ]). -spec is_supported(any()) -> boolean(). is_supported(integer) -> true; is_supported(string) -> true; is_supported(file) -> true; is_supported(directory) -> true; is_supported(flag) -> true; is_supported({flag, On, Off}) when is_atom(On), is_atom(Off) -> true; is_supported({flag, {On, _}, {Off, _}}) when is_atom(On), is_atom(Off) -> true; is_supported(atom) -> true; is_supported({enum, E}) when is_list(E) -> true; is_supported(ip) -> true; is_supported({duration, f}) -> true; is_supported({duration, w}) -> true; is_supported({duration, d}) -> true; is_supported({duration, h}) -> true; is_supported({duration, m}) -> true; is_supported({duration, s}) -> true; is_supported({duration, ms}) -> true; is_supported(bytesize) -> true; is_supported({percent, integer}) -> true; is_supported({percent, float}) -> true; is_supported(float) -> true; 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({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; is_extended({ip, {IP, Port}}) when is_list(IP) andalso is_integer(Port) -> true; is_extended({ip, StringIP}) when is_list(StringIP) -> true; is_extended({{duration, f}, D}) when is_list(D) -> true; is_extended({{duration, w}, D}) when is_list(D) -> true; is_extended({{duration, d}, D}) when is_list(D) -> true; is_extended({{duration, g}, D}) when is_list(D) -> true; is_extended({{duration, m}, D}) when is_list(D) -> true; is_extended({{duration, s}, D}) when is_list(D) -> true; is_extended({{duration, ms}, D}) when is_list(D) -> true; is_extended({bytesize, B}) when is_list(B) -> true; is_extended({{percent, integer}, _Int}) -> true; is_extended({{percent, float}, _Float}) -> true; is_extended({float, F}) when is_float(F) -> true; is_extended(_) -> false. -spec extended_from(extended()) -> datatype(). extended_from({integer, _}) -> integer; extended_from({string, _}) -> string; extended_from({atom, _}) -> atom; extended_from({file, _}) -> file; extended_from({directory, _}) -> directory; extended_from({ip, _}) -> ip; extended_from({{duration, Unit}, _}) -> {duration, Unit}; extended_from({bytesize, _}) -> bytesize; extended_from({{percent, integer}, _}) -> {percent, integer}; extended_from({{percent, float}, _}) -> {percent, float}; extended_from({float, _}) -> float; extended_from(Other) -> case is_supported(Other) of true -> Other; _ -> error end. -spec is_valid_list(any()) -> boolean(). is_valid_list(NotList) when not is_list(NotList) -> false; is_valid_list([]) -> false; is_valid_list(List) -> lists:all(fun(X) -> is_supported(X) orelse is_extended(X) end, List). -spec to_string(term(), datatype()) -> string() | cuttlefish_error:error(). to_string(Atom, atom) when is_list(Atom) -> Atom; to_string(Atom, atom) when is_atom(Atom) -> atom_to_list(Atom); to_string(Integer, integer) when is_integer(Integer) -> integer_to_list(Integer); to_string(Integer, integer) when is_list(Integer) -> Integer; to_string({IP, Port}, ip) when is_list(IP), is_integer(Port) -> IP ++ ":" ++ integer_to_list(Port); to_string(IPString, ip) when is_list(IPString) -> IPString; to_string(Enum, {enum, _}) when is_list(Enum) -> Enum; to_string(Enum, {enum, _}) when is_atom(Enum) -> atom_to_list(Enum); to_string(Duration, {duration, _}) when is_list(Duration) -> Duration; to_string(Duration, {duration, Unit}) when is_integer(Duration) -> cuttlefish_duration:to_string(Duration, Unit); to_string(Bytesize, bytesize) when is_list(Bytesize) -> Bytesize; to_string(Bytesize, bytesize) when is_integer(Bytesize) -> cuttlefish_bytesize:to_string(Bytesize); to_string(String, string) when is_list(String) -> String; to_string(File, file) when is_list(File) -> File; to_string(Directory, directory) when is_list(Directory) -> Directory; to_string(Flag, flag) when is_atom(Flag) -> cuttlefish_flag:to_string(Flag, flag); to_string(Flag, flag) when is_list(Flag) -> cuttlefish_flag:to_string(Flag, flag); to_string(Flag, {flag, _, _}=Type) when is_atom(Flag) -> cuttlefish_flag:to_string(Flag, Type); to_string(Flag, {flag, _, _}=Type) when is_list(Flag) -> cuttlefish_flag:to_string(Flag, Type); to_string(Percent, {percent, integer}) when is_integer(Percent) -> integer_to_list(Percent) ++ "%"; to_string(Percent, {percent, integer}) when is_list(Percent) -> Percent; to_string(Percent, {percent, float}) when is_float(Percent) -> P = list_to_float(float_to_list(Percent * 100, [{decimals, 6}, compact])), integer_to_list(cuttlefish_util:ceiling(P)) ++ "%"; to_string(Percent, {percent, float}) when is_list(Percent) -> Percent; to_string(Float, float) when is_float(Float) -> float_to_list(Float, [{decimals, 6}, compact]); to_string(Float, float) when is_list(Float) -> Float; %% The Pokemon Clause: Gotta Catch 'em all! to_string(Value, MaybeExtendedDatatype) -> case is_extended(MaybeExtendedDatatype) of true -> to_string(Value, extended_from(MaybeExtendedDatatype)); _ -> {error, {type, {Value, MaybeExtendedDatatype}}} end. -spec from_string(term(), datatype()) -> term() | cuttlefish_error:error(). from_string(Atom, atom) when is_atom(Atom) -> Atom; from_string(String, atom) when is_list(String) -> list_to_atom(String); from_string(Value, {enum, Enum}) -> cuttlefish_enum:parse(Value, {enum, Enum}); from_string(Integer, integer) when is_integer(Integer) -> Integer; from_string(String, integer) when is_list(String) -> try list_to_integer(String) of X -> X catch _:_ -> {error, {conversion, {String, integer}}} end; from_string({IP, Port}, ip) when is_list(IP), is_integer(Port) -> {IP, Port}; from_string(String, ip) when is_list(String) -> from_string_to_ip(String, lists:split(string:rchr(String, $:), String)); from_string(Duration, {duration, _}) when is_integer(Duration) -> Duration; from_string(Duration, {duration, Unit}) when is_list(Duration) -> cuttlefish_duration:parse(Duration, Unit); from_string(Bytesize, bytesize) when is_integer(Bytesize) -> Bytesize; from_string(Bytesize, bytesize) when is_list(Bytesize) -> cuttlefish_bytesize:parse(Bytesize); from_string(String, string) when is_list(String) -> String; from_string(File, file) when is_list(File) -> File; from_string(Directory, directory) when is_list(Directory) -> Directory; from_string(Flag, flag) when is_list(Flag) -> cuttlefish_flag:parse(Flag); from_string(Flag, flag) when is_atom(Flag) -> cuttlefish_flag:parse(Flag); from_string(Flag, {flag, _, _}=Type) when is_list(Flag) -> cuttlefish_flag:parse(Flag, Type); from_string(Flag, {flag, _, _}=Type) when is_atom(Flag) -> cuttlefish_flag:parse(Flag, Type); from_string(Percent, {percent, integer}) when is_integer(Percent), Percent >= 0, Percent =< 100 -> Percent; from_string(Percent, {percent, integer}) when is_integer(Percent) -> {error, {range, {{Percent, {percent, integer}}, "0 - 100%"}}}; %% This clause ends with a percent sign! from_string(Percent, {percent, integer}) when is_list(Percent) -> from_string( list_to_integer(string:sub_string(Percent, 1, length(Percent) - 1)), {percent, integer}); from_string(Percent, {percent, float}) when is_float(Percent), Percent >= 0, Percent =< 1 -> Percent; from_string(Percent, {percent, float}) when is_float(Percent) -> {error, {range, {{Percent, {percent, float}}, "0 - 100%"}}}; %% This clause ends with a percent sign! from_string(Percent, {percent, float}) when is_list(Percent) -> from_string( list_to_integer(string:sub_string(Percent, 1, length(Percent) - 1)) / 100.0, {percent, float}); from_string(Float, float) when is_float(Float) -> Float; from_string(String, float) when is_list(String) -> try list_to_float(String) of X -> X catch _:_ -> {error, {conversion, {String, float}}} end; from_string(Thing, InvalidDatatype) -> {error, {type, {Thing, InvalidDatatype}}}. %%% Utility functions for IP conversion port_to_integer(Str) -> try list_to_integer(Str) of X when X >= 0 -> X; %% Negative ports are nonsensical _X -> undefined catch _:_ -> undefined end. ip_conversions(String, _IPStr, {error, einval}, _Port) -> {error, {conversion, {String, 'IP'}}}; ip_conversions(String, _IPStr, _IP, undefined) -> {error, {conversion, {String, 'IP'}}}; ip_conversions(_String, IPStr, {ok, _}, Port) -> {IPStr, Port}. droplast(List) -> lists:sublist(List, length(List)-1). from_string_to_ip(String, {[], String}) -> {error, {conversion, {String, 'IP'}}}; %% No port from_string_to_ip(String, {IpPlusColon, PortString}) -> %% Still need to drop last character from IP, the trailing %% colon. Perfect use case for lists:droplast/1 but it's a recent %% addition IP = droplast(IpPlusColon), ip_conversions(String, IP, inet:parse_address(IP), port_to_integer(PortString)). -ifdef(TEST). -define(XLATE(X), lists:flatten(cuttlefish_error:xlate(X))). to_string_atom_test() -> ?assertEqual("split_the", to_string(split_the, atom)), ?assertEqual("split_the", to_string("split_the", atom)). to_string_integer_test() -> ?assertEqual("32", to_string(32, integer)), ?assertEqual("32", to_string("32", integer)). to_string_ip_test() -> ?assertEqual("127.0.0.1:8098", to_string("127.0.0.1:8098", ip)), ?assertEqual("127.0.0.1:8098", to_string({"127.0.0.1", 8098}, ip)). to_string_enum_test() -> ?assertEqual("true", to_string("true", {enum, [true, false]})), ?assertEqual("true", to_string(true, {enum, [true, false]})). to_string_string_test() -> ?assertEqual("string", to_string("string", string)). to_string_duration_test() -> ?assertEqual("1w", to_string("1w", {duration, s})), ?assertEqual("1w", to_string(604800000, {duration, ms})). to_string_bytesize_test() -> ?assertEqual("1GB", to_string(1073741824, bytesize)), ?assertEqual("1GB", to_string("1GB", bytesize)). to_string_percent_integer_test() -> ?assertEqual("10%", to_string(10, {percent, integer})), ?assertEqual("10%", to_string("10%", {percent, integer})), ok. to_string_percent_float_test() -> ?assertEqual("10%", to_string(0.1, {percent, float})), ?assertEqual("10%", to_string("10%", {percent, float})), ok. to_string_float_test() -> ?assertEqual("0.1", to_string(0.1, float)), ?assertEqual("0.1", to_string("0.1", float)), ok. to_string_extended_type_test() -> ?assertEqual("split_the", to_string(split_the, {atom, split_the})), ?assertEqual("split_the", to_string("split_the", {atom, split_the})), ?assertEqual("32", to_string(32, {integer, 32})), ?assertEqual("32", to_string("32", {integer, 32})), ?assertEqual("127.0.0.1:8098", to_string("127.0.0.1:8098", {ip, "127.0.0.1:8098"})), ?assertEqual("127.0.0.1:8098", to_string({"127.0.0.1", 8098}, {ip, {"127.0.0.1", 8098}})), ?assertEqual("string", to_string("string", {string, "string"})), ?assertEqual("1w", to_string("1w", {{duration, s}, "1w"})), ?assertEqual("1w", to_string(604800000, {{duration, ms}, "1w"})), ?assertEqual("1GB", to_string(1073741824, {bytesize, "1GB"})), ?assertEqual("1GB", to_string("1GB", {bytesize, "1GB"})), ?assertEqual("10%", to_string(10, {{percent, integer}, "10%"})), ?assertEqual("10%", to_string("10%", {{percent, integer}, "10%"})), ?assertEqual("10%", to_string(0.1, {{percent, float}, "10%"})), ?assertEqual("10%", to_string("10%", {{percent, float}, "10%"})), ?assertEqual("0.1", to_string(0.1, {float, 0.1})), ?assertEqual("0.1", to_string("0.1", {float, 0.1})), ok. to_string_unsupported_datatype_test() -> ?assertEqual("Tried to convert \"Something\" but invalid datatype: unsupported_datatype", ?XLATE(to_string("Something", unsupported_datatype))). from_string_atom_test() -> ?assertEqual(split_the, from_string(split_the, atom)), ?assertEqual(split_the, from_string("split_the", atom)). from_string_integer_test() -> ?assertEqual(32, from_string(32, integer)), ?assertEqual(32, from_string("32", integer)), ?assertEqual("\"thirty_two\" cannot be converted to a(n) integer", ?XLATE(from_string("thirty_two", integer))), ok. from_string_ip_test() -> ?assertEqual({"127.0.0.1", 8098}, from_string("127.0.0.1:8098", ip)), ?assertEqual( {"2001:0db8:85a3:0042:1000:8a2e:0370:7334", 8098}, from_string("2001:0db8:85a3:0042:1000:8a2e:0370:7334:8098", ip)), ?assertEqual( {"2001:0db8:85a3::0370:7334", 8098}, from_string("2001:0db8:85a3::0370:7334:8098", ip)), ?assertEqual( {"::1", 1}, from_string("::1:1", ip)), BadIPs = [ "This is not an IP:80", "2001:0db8:85a3:0042:1000:8a2e:0370:80", "127.0.0.1.1:80", "127.256.0.1:80", "127.0.0.1", %% No port "127.0.0.1:-5", "0:127.0.0.1:80", "127.0.0.1:80l", ":1:1" ], lists:foreach(fun(Bad) -> ?assertEqual({error, {conversion, {Bad, 'IP'}}}, from_string(Bad, ip)) end, BadIPs), ok. from_string_enum_test() -> ?assertEqual("\"a\" is not a valid enum value, acceptable values are: b, c", ?XLATE(from_string(a, {enum, [b, c]}))), ?assertEqual(true, from_string("true", {enum, [true, false]})), ?assertEqual(true, from_string(true, {enum, [true, false]})). from_string_duration_test() -> %% more examples in the the cuttlefish_duration tests ?assertEqual(1100, from_string("1s100ms", {duration, ms})), ?assertEqual(1100, from_string(1100, {duration, ms})), ok. from_string_duration_secs_test() -> %% more examples in the the cuttlefish_duration tests %% also rounds up for smaller units ?assertEqual(2, from_string("1s100ms", {duration, s})), ?assertEqual(2, from_string(2, {duration, s})), ok. from_string_percent_integer_test() -> ?assertEqual(10, from_string("10%", {percent, integer})), ?assertEqual(10, from_string(10, {percent, integer})), %% Range! ?assertEqual(0, from_string("0%", {percent, integer})), ?assertEqual(100, from_string("100%", {percent, integer})), ?assertEqual("110% can't be outside the range 0 - 100%", ?XLATE(from_string("110%", {percent, integer}))), ?assertEqual("-1% can't be outside the range 0 - 100%", ?XLATE(from_string("-1%", {percent, integer}))), ok. from_string_percent_float_test() -> ?assertEqual(0.10, from_string("10%", {percent, float})), ?assertEqual(0.10, from_string(0.1, {percent, float})), %% Range! ?assertEqual(0.0, from_string("0%", {percent, float})), ?assertEqual(1.0, from_string("100%", {percent, float})), ?assertEqual("110% can't be outside the range 0 - 100%", ?XLATE(from_string("110%", {percent, float}))), ?assertEqual("-1% can't be outside the range 0 - 100%", ?XLATE(from_string("-1%", {percent, float}))), ok. from_string_float_test() -> ?assertEqual(0.1, from_string("0.100", float)), ?assertEqual(0.1, from_string(0.1, float)), ok. from_string_string_test() -> ?assertEqual("string", from_string("string", string)). from_string_unsupported_datatype_test() -> ?assertEqual("Tried to convert \"string\" but invalid datatype: unsupported_datatype", ?XLATE(from_string("string", unsupported_datatype))). is_supported_test() -> ?assert(is_supported(integer)), ?assert(is_supported(string)), ?assert(is_supported(atom)), ?assert(is_supported(file)), ?assert(is_supported(directory)), ?assert(is_supported({enum, [one, two, three]})), ?assert(not(is_supported({enum, not_a_list}))), ?assert(is_supported(ip)), ?assert(is_supported({duration, f})), ?assert(is_supported({duration, w})), ?assert(is_supported({duration, d})), ?assert(is_supported({duration, h})), ?assert(is_supported({duration, m})), ?assert(is_supported({duration, s})), ?assert(is_supported({duration, ms})), ?assert(is_supported(bytesize)), ?assert(not(is_supported(some_unsupported_type))), ok. is_extended_test() -> ?assertEqual(true, is_extended({integer, 10})), ?assertEqual(true, is_extended({integer, -10})), ?assertEqual(false, is_extended({integer, "ten"})), ?assertEqual(true, is_extended({string, "string"})), ?assertEqual(false, is_extended({string, string})), ?assertEqual(false, is_extended({string, 10})), ?assertEqual(true, is_extended({atom, atom})), ?assertEqual(false, is_extended({atom, "atom"})), ?assertEqual(false, is_extended({atom, 10})), ?assertEqual(true, is_extended({file, "/tmp/foo.txt"})), ?assertEqual(true, is_extended({file, ""})), ?assertEqual(false, is_extended({file, this})), ?assertEqual(true, is_extended({directory, "/tmp/foo.txt"})), ?assertEqual(true, is_extended({directory, ""})), ?assertEqual(false, is_extended({directory, this})), ?assertEqual(true, is_extended({ip, {"1.2.3.4", 1234}})), ?assertEqual(false, is_extended({ip, {1234, 1234}})), ?assertEqual(false, is_extended({ip, {"1.2.3.4", "1234"}})), ?assertEqual(true, is_extended({{duration, f}, "10f"})), ?assertEqual(true, is_extended({{duration, w}, "10f"})), ?assertEqual(true, is_extended({{duration, d}, "10f"})), ?assertEqual(true, is_extended({{duration, g}, "10f"})), ?assertEqual(true, is_extended({{duration, m}, "10f"})), ?assertEqual(true, is_extended({{duration, s}, "10f"})), ?assertEqual(true, is_extended({{duration, ms}, "10ms"})), ?assertEqual(true, is_extended({bytesize, "10GB"})), ?assertEqual(true, is_extended({{percent, integer}, "10%"})), ?assertEqual(true, is_extended({{percent, integer}, 10})), ?assertEqual(true, is_extended({{percent, float}, "10%"})), ?assertEqual(true, is_extended({{percent, float}, 0.1})), ?assertEqual(true, is_extended({float, 0.1})), ok. -endif.