Browse Source

Support for record variables.

Fixes #105.

In order for records to be recognized, the record fields info needs to
be passed to erlydtl when the template is being compiled.
Andreas Stenius 11 years ago
parent
commit
bec899d4dc

+ 4 - 0
README.markdown

@@ -111,6 +111,10 @@ lists). Defaults to `true`.
 
 * `verbose` - Enable verbose printing of compilation results.
 
+* `record_info` - List of records to look for when rendering the template. Each record info is a tuple with the fields of the record:
+
+    {my_record, record_info(fields, my_record)}
+
 
 Additional compiler options can be provided with the `ERLYDTL_COMPILER_OPTIONS`
 OS environment variable.

+ 1 - 0
include/erlydtl_ext.hrl

@@ -8,6 +8,7 @@
           doc_root = "", 
           parse_trail = [],
           vars = [],
+          record_info = [],
           filter_modules = [],
           custom_tags_dir = [],
           custom_tags_modules = [],

+ 36 - 18
src/erlydtl_compiler.erl

@@ -281,7 +281,9 @@ init_context(IsCompilingDir, ParseTrail, DefDir, Module, Options) ->
                  verbose = proplists:get_value(verbose, Options, Ctx#dtl_context.verbose),
                  is_compiling_dir = IsCompilingDir,
                  extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
-                 scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module)
+                 scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
+                 record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
+                                || {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)]
                 },
     case call_extension(Context, init_context, [Context]) of
         {ok, C} when is_record(C, dtl_context) -> C;
@@ -680,12 +682,14 @@ options_match_ast(Context, TreeWalker) ->
        erl_syntax:application(
          erl_syntax:atom(proplists),
          erl_syntax:atom(get_value),
-         [erl_syntax:atom(locale), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)]))
+         [erl_syntax:atom(locale), erl_syntax:variable("RenderOptions"), erl_syntax:atom(none)])),
+     erl_syntax:match_expr(
+       erl_syntax:variable("_RecordInfo"),
+       erl_syntax:abstract(Context#dtl_context.record_info))
      | case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
            undefined -> [];
            Ast when is_list(Ast) -> Ast
-       end
-    ].
+       end].
 
                                                 % child templates should only consist of blocks at the top level
 body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], Context, TreeWalker) ->
@@ -1211,12 +1215,14 @@ resolve_variable_ast(VarTuple, Context, TreeWalker, EmptyIfUndefined)
 resolve_variable_ast(VarTuple, Context, TreeWalker, FinderFunction) ->
     resolve_variable_ast1(VarTuple, Context, TreeWalker, FinderFunction).
 
-resolve_variable_ast1({attribute, {{AttrKind, {Row, Col}, Attr}, Variable}}, Context, TreeWalker, FinderFunction) ->
+resolve_variable_ast1({attribute, {{AttrKind, Pos, Attr}, Variable}}, Context, TreeWalker, FinderFunction) ->
     {{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, Context, TreeWalker, FinderFunction),
-    FileNameAst = case Context#dtl_context.parse_trail of
-                      [] -> erl_syntax:atom(undefined);
-                      [H|_] -> erl_syntax:string(H)
-                  end,
+    FileNameAst = erl_syntax:tuple(
+                    [erl_syntax:atom(filename),
+                     case Context#dtl_context.parse_trail of
+                         [] -> erl_syntax:atom(undefined);
+                         [H|_] -> erl_syntax:string(H)
+                     end]),
     AttrAst = erl_syntax:abstract(
                 case AttrKind of
                     number_literal -> erlang:list_to_integer(Attr);
@@ -1226,24 +1232,36 @@ resolve_variable_ast1({attribute, {{AttrKind, {Row, Col}, Attr}, Variable}}, Con
     {{erl_syntax:application(
         erl_syntax:atom(Runtime),
         erl_syntax:atom(Finder),
-        [AttrAst, VarAst, FileNameAst,
-         erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)])
+        [AttrAst, VarAst,
+         erl_syntax:list(
+           [FileNameAst,
+            erl_syntax:abstract({pos, Pos}),
+            erl_syntax:tuple([erl_syntax:atom(record_info),
+                              erl_syntax:variable("_RecordInfo")])
+           ])
         ]),
       VarInfo},
      TreeWalker1};
 
-resolve_variable_ast1({variable, {identifier, {Row, Col}, VarName}}, Context, TreeWalker, FinderFunction) ->
+resolve_variable_ast1({variable, {identifier, Pos, VarName}}, Context, TreeWalker, FinderFunction) ->
     VarValue = case resolve_scoped_variable_ast(VarName, Context) of
                    undefined ->
-                       FileNameAst = case Context#dtl_context.parse_trail of
-                                         [] -> erl_syntax:atom(undefined);
-                                         [H|_] -> erl_syntax:string(H)
-                                     end,
+                       FileNameAst = erl_syntax:tuple(
+                                       [erl_syntax:atom(filename),
+                                        case Context#dtl_context.parse_trail of
+                                            [] -> erl_syntax:atom(undefined);
+                                            [H|_] -> erl_syntax:string(H)
+                                        end]),
                        {Runtime, Finder} = FinderFunction,
                        erl_syntax:application(
                          erl_syntax:atom(Runtime), erl_syntax:atom(Finder),
-                         [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"), FileNameAst,
-                          erl_syntax:tuple([erl_syntax:integer(Row), erl_syntax:integer(Col)])
+                         [erl_syntax:atom(VarName), erl_syntax:variable("_Variables"),
+                          erl_syntax:list(
+                            [FileNameAst,
+                             erl_syntax:abstract({pos, Pos}),
+                             erl_syntax:tuple([erl_syntax:atom(record_info),
+                                               erl_syntax:variable("_RecordInfo")])
+                            ])
                          ]);
                    Val ->
                        Val

+ 14 - 3
src/erlydtl_runtime.erl

@@ -4,7 +4,18 @@
 
 -define(IFCHANGED_CONTEXT_VARIABLE, erlydtl_ifchanged_context).
 
-find_value(Key, Data, _, _) ->
+find_value(Key, Data, Options) when is_atom(Key), is_tuple(Data) ->
+    Rec = element(1, Data),
+    Info = proplists:get_value(record_info, Options),
+    case proplists:get_value(Rec, Info) of
+        Fields when is_list(Fields), length(Fields) == size(Data) - 1 ->
+            case proplists:get_value(Key, Fields) of
+                Idx when is_integer(Idx) -> element(Idx, Data);
+                _ -> undefined
+            end;
+        _ -> find_value(Key, Data)
+    end;
+find_value(Key, Data, _Options) ->
     find_value(Key, Data).
 
 find_value(_, undefined) ->
@@ -67,8 +78,8 @@ find_value(Key, Tuple) when is_tuple(Tuple) ->
             end
     end.
 
-fetch_value(Key, Data, _FileName, _Pos) ->
-    case find_value(Key, Data) of
+fetch_value(Key, Data, Options) ->
+    case find_value(Key, Data, Options) of
         undefined -> [];
         Val -> Val
     end.

+ 36 - 35
tests/src/erlydtl_functional_tests.erl

@@ -39,16 +39,16 @@
 -export([run_tests/0, run_test/1]).
 
 test_list() ->
-% order is important.
+    %% order is important.
     ["autoescape", "comment", "extends", "filters", "for", "for_list",
-        "for_tuple", "for_list_preset", "for_preset", "for_records",
-        "for_records_preset", "include", "if", "if_preset", "ifequal",
-        "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
-        "var", "var_preset", "cycle",
-        "custom_tag", "custom_tag1", "custom_tag2", "custom_tag3", "custom_tag4",
-        "custom_call", 
-        "include_template", "include_path", "ssi",
-        "extends_path", "extends_path2", "trans", "extends2", "extends3" ].
+     "for_tuple", "for_list_preset", "for_preset", "for_records",
+     "for_records_preset", "include", "if", "if_preset", "ifequal",
+     "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
+     "var", "var_preset", "cycle",
+     "custom_tag", "custom_tag1", "custom_tag2", "custom_tag3", "custom_tag4",
+     "custom_call",
+     "include_template", "include_path", "ssi",
+     "extends_path", "extends_path2", "trans", "extends2", "extends3" ].
 
 setup_compile("for_list_preset") ->
     CompileVars = [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}],
@@ -60,7 +60,7 @@ setup_compile("for_records_preset") ->
     Link1a = [{name, "Amazon (preset)"}, {url, "http://amazon.com"}],
     Link2a = [{name, "Google (preset)"}, {url, "http://google.com"}],
     Link3a = [{name, "Microsoft (preset)"}, {url, "http://microsoft.com"}],
-    CompileVars = [{software_links, [Link1a, Link2a, Link3a]}], 
+    CompileVars = [{software_links, [Link1a, Link2a, Link3a]}],
     {ok, CompileVars};
 setup_compile("if_preset") ->
     CompileVars = [{var1, "something"}],
@@ -83,23 +83,23 @@ setup_compile("extends3") ->
 setup_compile(_) ->
     {ok, []}.
 
-%% @spec (Name::string()) -> {CompileStatus::atom(), PresetVars::list(), 
+%% @spec (Name::string()) -> {CompileStatus::atom(), PresetVars::list(),
 %%     RenderStatus::atom(), RenderVars::list()} | skip
 %% @doc
-%% @end 
+%% @end
 %%--------------------------------------------------------------------
 setup("autoescape") ->
     RenderVars = [{var1, "<b>bold</b>"}],
-    {ok, RenderVars};  
+    {ok, RenderVars};
 setup("extends") ->
     RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
     {ok, RenderVars};
 setup("filters") ->
     RenderVars = [
-        {date_var1, {1975,7,24}},
-        {datetime_var1, {{1975,7,24}, {7,13,1}}},
-        {'list', ["eins", "zwei", "drei"]}
-    ],
+                  {date_var1, {1975,7,24}},
+                  {datetime_var1, {{1975,7,24}, {7,13,1}}},
+                  {'list', ["eins", "zwei", "drei"]}
+                 ],
     {ok, RenderVars};
 setup("for") ->
     RenderVars = [{fruit_list, ["apple", "banana", "coconut"]}],
@@ -115,7 +115,7 @@ setup("for_records") ->
     Link2 = [{name, "Google"}, {url, "http://google.com"}],
     Link3 = [{name, "Microsoft"}, {url, "http://microsoft.com"}],
     RenderVars = [{link_list, [Link1, Link2, Link3]}],
-    {ok, RenderVars};  
+    {ok, RenderVars};
 setup("for_records_preset") ->
     Link1b = [{name, "Canon"}, {url, "http://canon.com"}],
     Link2b = [{name, "Leica"}, {url, "http://leica.com"}],
@@ -127,16 +127,16 @@ setup("include") ->
     {ok, RenderVars};
 setup("if") ->
     RenderVars = [{var1, "something"}],
-    {ok, RenderVars}; 
+    {ok, RenderVars};
 setup("ifequal") ->
     RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
-    {ok, RenderVars};      
+    {ok, RenderVars};
 setup("ifequal_preset") ->
     RenderVars = [{var3, "bar"}],
-    {ok, RenderVars};   
+    {ok, RenderVars};
 setup("ifnotequal") ->
     RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
-    {ok, RenderVars};        
+    {ok, RenderVars};
 setup("now") ->
     {ok, [], [], skip_check};
 setup("var") ->
@@ -144,7 +144,7 @@ setup("var") ->
     {ok, RenderVars};
 setup("var_preset") ->
     RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}],
-    {ok, RenderVars}; 
+    {ok, RenderVars};
 setup("cycle") ->
     RenderVars = [{test, [integer_to_list(X) || X <- lists:seq(1, 20)]},
                   {a, "Apple"}, {b, "Banana"}, {c, "Cherry"}],
@@ -179,18 +179,18 @@ setup("ssi") ->
     {ok, RenderVars};
 
 
-%%--------------------------------------------------------------------       
+%%--------------------------------------------------------------------
 %% Custom tags
 %%--------------------------------------------------------------------
 setup("custom_call") ->
     RenderVars = [{var1, "something"}],
-    {ok, RenderVars};    
+    {ok, RenderVars};
 
 setup(_) ->
     {ok, []}.
-    
 
-run_tests() ->    
+
+run_tests() ->
     io:format("Running functional tests...~n"),
     case [filelib:ensure_dir(
             filename:join([templates_dir(Dir), "foo"]))
@@ -239,16 +239,17 @@ fold_tests() ->
                         io:format("~n"), Res
                 end, {0, []}, test_list()).
 
-test_compile_render(Name) ->  
+test_compile_render(Name) ->
     File = filename:join([templates_docroot(), Name]),
     Module = "functional_test_" ++ Name,
     io:format(" Template: ~p, ... ", [Name]),
     case setup_compile(Name) of
         {CompileStatus, CompileVars} ->
             Options = [
-                {vars, CompileVars}, 
-                {force_recompile, true},
-                {custom_tags_modules, [erlydtl_custom_tags]}],
+                       {vars, CompileVars},
+                       {force_recompile, true},
+                       %% {compiler_options, [debug_compiler]},
+                       {custom_tags_modules, [erlydtl_custom_tags]}],
             io:format("compiling ... "),
             case erlydtl:compile(File, Module, Options) of
                 ok ->
@@ -259,7 +260,7 @@ test_compile_render(Name) ->
                     end;
                 {error, _}=Err ->
                     if CompileStatus =:= Err -> io:format("ok");
-                       true -> 
+                       true ->
                             io:format("failed"),
                             {compile_error, io_lib:format("~p", [Err])}
                     end
@@ -275,13 +276,13 @@ test_render(Name, Module) ->
             {RS, V, O}    -> {RS, V, O, get_expected_result(Name)};
             {RS, V, O, R} -> {RS, V, O, R}
         end,
-    io:format("rendering ... "), 
+    io:format("rendering ... "),
     case catch Module:render(Vars, Opts) of
         {ok, Output} ->
             Data = iolist_to_binary(Output),
             if RenderStatus =:= ok ->
                     if RenderResult =:= undefined ->
-                            Devs = [begin 
+                            Devs = [begin
                                         FileName = filename:join([templates_dir(Dir), Name]),
                                         {ok, IoDev} = file:open(FileName, [write]),
                                         IoDev
@@ -316,7 +317,7 @@ test_render(Name, Module) ->
                true -> io:format("failed"),
                        {render_error, io_lib:format("~p", [Err])}
             end
-    end.   
+    end.
 
 get_expected_result(Name) ->
     FileName = filename:join([templates_dir("expect"), Name]),

+ 19 - 0
tests/src/erlydtl_unittests.erl

@@ -2,8 +2,21 @@
 
 -export([run_tests/0]).
 
+-record(testrec, {foo, bar, baz}).
+
 tests() ->
     [
+     %% {"scanner",
+     %%  [{"multiline tags", %% weird formatting example from issue #103.
+     %%    <<"{% if a \n"
+     %%      "  %}{% if a.b \n"
+     %%      "    %}{{ a.b \n"
+     %%      "    }}{% endif\n"
+     %%      "  %}{% endif\n"
+     %%      "%}">>,
+     %%    [{a, [{b, 123}]}],
+     %%    <<"...">>} %% dtl compat: expect the whole input text, tags and all..
+     %%  ]},
      {"vars", [
                {"string",
                 <<"String value is: {{ var1 }}">>,
@@ -1209,6 +1222,12 @@ tests() ->
         %% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility)
         {"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [],
          [{extension_module, erlydtl_extension_test}], <<"ok">>}
+      ]},
+     {"records",
+      [{"field access",
+        <<"{{ r.baz }}">>, [{r, #testrec{ foo="Foo", bar="Bar", baz="Baz" }}], [],
+        [{record_info, [{testrec, record_info(fields, testrec)}]}],
+        <<"Baz">>}
       ]}
     ].