@@ -205,6 +205,12 @@ defmodule Spark.Options do
205205
206206 * `{:fun, args_types, return_type}` - A function with the specified arguments and return type.
207207
208+ * `{:function, arity: n, args: [...], returns: type}` - A function type with named options. All keys are optional.
209+ `:arity` specifies the arity, `:args` specifies argument types, and `:returns` specifies the return type.
210+ For example, `{:function, args: [:map], returns: :string}` is a function taking a map and returning a string.
211+ This format is preferred over the 3-tuple `{:fun, args, return_type}` as it is a 2-tuple and avoids
212+ AST escaping issues when used in nested type positions.
213+
208214 * `{:in, choices}` or `{:one_of, choices}` - A value that is a member of one of the `choices`. `choices`
209215 should be a list of terms or a `Range`. The value is an element in said
210216 list of terms, that is, `value in choices` is `true`.
@@ -1220,6 +1226,11 @@ defmodule Spark.Options do
12201226 )
12211227 end
12221228
1229+ defp validate_type ( { :function , opts } , key , value ) when is_list ( opts ) do
1230+ arity = opts [ :arity ] || length ( opts [ :args ] || [ ] )
1231+ validate_type ( { :fun , arity } , key , value )
1232+ end
1233+
12231234 defp validate_type ( nil , _key , nil ) , do: { :ok , nil }
12241235
12251236 defp validate_type ( nil , key , value ) ,
@@ -1707,7 +1718,8 @@ defmodule Spark.Options do
17071718 "{:list, subtype}" ,
17081719 "{:tuple, list_of_subtypes}" ,
17091720 "{:map, key_type, value_type}" ,
1710- "{:struct, struct_name}"
1721+ "{:struct, struct_name}" ,
1722+ "{:function, arity: n, args: [...], returns: type}"
17111723 ]
17121724
17131725 Enum . join ( types , ", " )
@@ -1738,17 +1750,7 @@ defmodule Spark.Options do
17381750 end
17391751
17401752 def validate_type ( { :fun , list } ) when is_list ( list ) do
1741- Enum . reduce_while ( list , { :ok , list } , fn
1742- { type , _keys } , acc
1743- when type in [ :keyword_list , :non_empty_keyword_list , :map ] ->
1744- { :cont , acc }
1745-
1746- subtype , acc ->
1747- case validate_type ( subtype ) do
1748- { :ok , _value } -> { :cont , acc }
1749- { :error , reason } -> { :halt , { :error , "invalid type given to :fun type: #{ reason } " } }
1750- end
1751- end )
1753+ validate_fun_arg_types ( list )
17521754 end
17531755
17541756 def validate_type ( { :fun , list , returns } ) when is_list ( list ) do
@@ -1778,6 +1780,35 @@ defmodule Spark.Options do
17781780 end
17791781 end
17801782
1783+ def validate_type ( { :function , opts } ) when is_list ( opts ) do
1784+ arity = opts [ :arity ]
1785+ args = opts [ :args ]
1786+ returns = opts [ :returns ]
1787+
1788+ extra_keys = Keyword . keys ( opts ) -- [ :arity , :args , :returns ]
1789+
1790+ cond do
1791+ extra_keys != [ ] ->
1792+ { :error ,
1793+ "invalid keys #{ inspect ( extra_keys ) } in function type. Only :arity, :args, and :returns are supported" }
1794+
1795+ arity != nil && ( ! is_integer ( arity ) || arity < 0 ) ->
1796+ { :error , "expected :arity to be a non-negative integer, got: #{ inspect ( arity ) } " }
1797+
1798+ args != nil && ! is_list ( args ) ->
1799+ { :error , "expected :args to be a list of types, got: #{ inspect ( args ) } " }
1800+
1801+ arity != nil && args != nil && length ( args ) != arity ->
1802+ { :error , "expected :args length (#{ length ( args ) } ) to match :arity (#{ arity } )" }
1803+
1804+ true ->
1805+ with { :ok , _ } <- validate_fun_args ( args || [ ] ) ,
1806+ { :ok , _ } <- validate_fun_returns ( returns ) do
1807+ { :ok , { :function , opts } }
1808+ end
1809+ end
1810+ end
1811+
17811812 def validate_type ( { :spark_type , module , builtin_function } = type )
17821813 when is_atom ( module ) and is_atom ( builtin_function ) do
17831814 { :ok , type }
@@ -1950,6 +1981,43 @@ defmodule Spark.Options do
19501981 { :error , "unknown type #{ inspect ( value ) } .\n \n Available types: #{ available_types ( ) } " }
19511982 end
19521983
1984+ defp validate_fun_arg_types ( list ) do
1985+ Enum . reduce_while ( list , { :ok , list } , fn
1986+ { type , _keys } , acc
1987+ when type in [ :keyword_list , :non_empty_keyword_list , :map ] ->
1988+ { :cont , acc }
1989+
1990+ subtype , acc ->
1991+ case validate_type ( subtype ) do
1992+ { :ok , _value } -> { :cont , acc }
1993+ { :error , reason } -> { :halt , { :error , "invalid type given to :fun type: #{ reason } " } }
1994+ end
1995+ end )
1996+ end
1997+
1998+ defp validate_fun_args ( args ) do
1999+ Enum . reduce_while ( args , { :ok , args } , fn
2000+ { type , _keys } , acc
2001+ when type in [ :keyword_list , :non_empty_keyword_list , :map ] ->
2002+ { :cont , acc }
2003+
2004+ subtype , acc ->
2005+ case validate_type ( subtype ) do
2006+ { :ok , _value } -> { :cont , acc }
2007+ { :error , reason } -> { :halt , { :error , "invalid type given to :fun type: #{ reason } " } }
2008+ end
2009+ end )
2010+ end
2011+
2012+ defp validate_fun_returns ( nil ) , do: { :ok , nil }
2013+
2014+ defp validate_fun_returns ( returns ) do
2015+ case validate_type ( returns ) do
2016+ { :ok , _ } -> { :ok , returns }
2017+ { :error , error } -> { :error , "invalid return type given to :fun type: #{ error } " }
2018+ end
2019+ end
2020+
19532021 defp error_tuple ( key , value , message ) do
19542022 { :error , % ValidationError { key: key , message: message , value: value } }
19552023 end
0 commit comments