diff --git a/LICENSE.md b/LICENSE.md index 09f8c57..61e3595 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,186 +1,20 @@ -# Apache License -Version 2.0, January 2004 +Copyright 2023 Mathieu Kerjouan -http://www.apache.org/licenses/ +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -## 1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -## 2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -## 3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -## 4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -1. You must give any other recipients of the Work or Derivative Works a copy of - this License; and - -2. You must cause any modified files to carry prominent notices stating that - You changed the files; and - -3. You must retain, in the Source form of any Derivative Works that You - distribute, all copyright, patent, trademark, and attribution notices from - the Source form of the Work, excluding those notices that do not pertain to - any part of the Derivative Works; and - -4. If the Work includes a "NOTICE" text file as part of its distribution, then - any Derivative Works that You distribute must include a readable copy of the - attribution notices contained within such NOTICE file, excluding those - notices that do not pertain to any part of the Derivative Works, in at least - one of the following places: within a NOTICE text file distributed as part - of the Derivative Works; within the Source form or documentation, if - provided along with the Derivative Works; or, within a display generated by - the Derivative Works, if and wherever such third-party notices normally - appear. The contents of the NOTICE file are for informational purposes only - and do not modify the License. You may add Your own attribution notices - within Derivative Works that You distribute, alongside or as an addendum to - the NOTICE text from the Work, provided that such additional attribution - notices cannot be construed as modifying the License. - -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -## 5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -## 6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -## 7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, NON- -INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -## 8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -## 9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -Copyright 2023, Mathieu Kerjouan . - -Licensed 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. \ No newline at end of file +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 47febdc..e1ba969 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,15 @@ # gabarit +![GitHub top language](https://img.shields.io/github/languages/top/erlang-punch/gabarit) +![GitHub last commit (branch)](https://img.shields.io/github/last-commit/erlang-punch/gabarit/master) +![GitHub contributors](https://img.shields.io/github/contributors/erlang-punch/gabarit) +![GitHub all releases](https://img.shields.io/github/downloads/erlang-punch/gabarit/total) +![GitHub repo size](https://img.shields.io/github/repo-size/erlang-punch/gabarit) + Gabarit, a template engine manager for Erlang/OTP. +**!!!This is a draft, please don't use it in production!!!** + Gabarit was created to offer an easy way to deal with template engines from the Erlang community but also to deal with multi-template engines running on the same cluster. @@ -15,8 +23,8 @@ templates using merl. ## Features - - [ ] templates versionned during compilation - - [ ] templates compiled with timestamp + - [x] templates versionned during compilation + - [x] templates compiled with timestamp - [ ] hardcoded templates limits - [ ] templates compilation limit during runtime - [ ] automatic or dynamic reload @@ -24,6 +32,9 @@ templates using merl. - [ ] rollback template based on version - [ ] export feature - [ ] template edition facility (text editor integration) + - [ ] namespace support + - [ ] default value and options based on namespace + - [ ] partial templating - [ ] project manager support - [ ] rebar3 plugin - [ ] erlang.mk plugin @@ -150,8 +161,121 @@ gabarit:render("path", #{}, []). gabarit:edit("path"). ``` -## Limitations +## Ideas + +This project is side project created to play and improve template +management on Erlang/OTP system. Here list of ideas we would like to +work on. + +### In Memory Compressed Templates + +Templates can be really huge, and compressing them can save lot of +place, even more if they are using in a loaded module in the BEAM. + +### Limitations Erlang modules are compiled using atoms, atoms are limited and not garbage collected. That means it could crash a production system if -you want to dynamically +you want to dynamically. + +### Partial Templates + +Sometimes, only a small part of the template needs to be dynamic, the +other part will stay static. Partial template will create a partial +template module with already replaced values in it. + +### Development Environment + +In development environment, templates can easily be upgraded and +updated on the fly, with local version. When one or more templates are +validated by developers, they can be exported with a fixed version. + +### Production Environment + +In production environment, templates are locked and can't be updated +on the fly, except during an explicit update/upgrade of the +application. + +### Distributed Templates + +In distributed environment, templates must be available on each nodes +requiring this module, even if the distribution does not have access +to them locally. We should probably also provide a RPC function. + +### Custom Template Engine + +A custom template engine with an AST representation and many way to +modify them, but without logic in it (avoid using if, for, while +expression). It should be developed in pure Erlang. + +### Structured and Unstructured Template Engine Support + +It could be great to support both structured and unstructured template +engines to generate, for example, XML, HTML, JSON but also Mardown and +raw textual files. + +### What About Binary Template Engine? + +We are used to use textual templates, but rarely binary templates. In +fact, having binary template could be an extension of pattern matching +in Erlang, when one specific binary pattern can be replaced, extended, +removed on some part in data. I don't think it should be part of this +project, but could be a great idea to implement. What kind of usage? +Network package payload modification on the fly, data fuzzing and so +on. Here a code example of what I have in mind: + +```erlang +% A mapping is a document containing address on a binary map with +% default instructions when the pattern needs to be changed. +Identifier = "identifier". +Mapping = [{Identifier, {From, To}} + ,{Identifier2, {From, {absolute, To}}, Opts2} + ,{Identifier3, {From, {relative, To}}, Opts3} + ,{Identifier4, {From, To}, [reversed]} + % set an elf_magic identifier to replace the magic + % number used by an executable binary file using + % ELF. It will be a fixed size. + ,{"elf_magic", {16#00, 16#05}, [fixed]} + ]. +{ok, Cat} = file:read_file("/bin/cat"). +{ok, Module} = gabarit_binary:compile(Cat, Mapping). + +% returns the data +Module:data(). + +Module:analyze(). +Module:analyze(fun(X) -> + +% print the default mapping used for this file +Module:mapping(). + +% replace a pattern by a term +Module:replace(Identifier, Term). + +% delete a pattern and resize the payload +Module:delete(Identifier). + +% nullify a pattern (replace by 0x00). +Module:nullify(Identifier). + +% randomize a pattern (replace with random payload) +Module:randomize(Identifier). + +% clone a module with new identifier and/or already +% replaced values +Module:clone([{Identifier, {replace, Term, Opts}}] + ,[{"newid", {From, To}, Opts}]). + +% manual replacement +% {replace, {From, To}, Term, Opts} +% {nullify, {From, To}, Opts} +% {delete, {From, To}, Opts} +% {randomize, {From, To}, Opts} +Module:apply(Command, Opts). +Module:apply([Command, Command], Opts). + +% some function to apply on the loaded payload +Module:map(fun(Integer) -> Integer end, Size). +Module:reduce(fun(Integer, BinAcc) -> <> end, Size). + +``` diff --git a/src/gabarit.erl b/src/gabarit.erl index 1802655..fc0810d 100644 --- a/src/gabarit.erl +++ b/src/gabarit.erl @@ -1 +1,67 @@ +%%%=================================================================== +%%% @doc +%%% @end +%%%=================================================================== -module(gabarit). +-export([new/1]). +-export([template/1]). +-export([format/2, format/3]). +-export([render/2, render/3]). + +%%-------------------------------------------------------------------- +%% @doc Create a new template from file present in default template +%% path. +%% +%% == Example == +%% +%% ``` +%% {ok, Module} = gabaric:new("index.html"). +%% ''' +%% +%% @end +%%-------------------------------------------------------------------- +-spec new(Filename) -> Return when + Filename :: string() | binary(), + Return :: {ok, Module}, + Module :: atom(). +new(Filename) -> + Template = gabarit_compiler:template_file(Filename), + gabarit_compiler:compile_file(Template). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +template(Filename) -> + ModuleName = gabarit_compiler:find_template_file_module(Filename), + ModuleName:template(). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +format(Filename, Context) -> + format(Filename, Context, []). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +format(Filename, Context, Opts) -> + ModuleName = gabarit_compiler:find_template_file_module(Filename), + ModuleName:format(Context, Opts). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +render(Filename, Context) -> + render(Filename, Context, []). + +%%-------------------------------------------------------------------- +%% @doc +%% @end +%%-------------------------------------------------------------------- +render(Filename, Context, Opts) -> + ModuleName = gabarit_compiler:find_template_file_module(Filename), + ModuleName:render(Context, Opts). diff --git a/src/gabarit_compiler.erl b/src/gabarit_compiler.erl index 18190c4..84e20fa 100644 --- a/src/gabarit_compiler.erl +++ b/src/gabarit_compiler.erl @@ -1,26 +1,47 @@ %%%=================================================================== -%%% @doc +%%% @doc private interface module. %%% @end %%%=================================================================== -module(gabarit_compiler). -compile(export_all). -export([path/0, tree/0, tree/1]). -define(DEFAULT_PATH, "priv/templates"). +-define(DEFAULT_PREFIX_FILE, "gabarit@"). +-define(DEFAULT_PREFIX_STRING, "gabarit$"). -define(DEFAULT_MERL_TEMPLATE, "priv/gabarit/gabarit_template.erl"). +%%-------------------------------------------------------------------- +%% @doc compile +%% @end +%%-------------------------------------------------------------------- +compile_file(#{ name := Identifier } = Struct) -> + ModuleName = create_module_name(?DEFAULT_PREFIX_FILE, Identifier), + NewStruct = Struct#{ module_name => ModuleName }, + case merl_compile_and_load(NewStruct) of + {ok, _} -> + {ok, ModuleName}; + Elsewise -> Elsewise + end. + %%-------------------------------------------------------------------- %% %%-------------------------------------------------------------------- -compile(#{ name := Identifier } = Struct) -> - ModuleName = create_module_name("gabarit@", Identifier), +compile_string(#{ name := Identifier } = Struct) -> + ModuleName = create_module_name(?DEFAULT_PREFIX_STRING, Identifier), merl_compile_and_load(Struct#{ module_name => ModuleName }). +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +compile(Struct) when is_map(Struct) -> + merl_compile_and_load(Struct). + %%-------------------------------------------------------------------- %% %%-------------------------------------------------------------------- find_and_compile() -> Tree = tree(), - lists:map(fun compile/1, Tree). + lists:map(fun compile_file/1, Tree). %%-------------------------------------------------------------------- %% @@ -42,18 +63,29 @@ create_module_name(Prefix, Identifier) -> _:_ -> erlang:list_to_atom(ModuleName) end. +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +find_template_file_module(Identifier) -> + ModuleName = string:concat(?DEFAULT_PREFIX_FILE, Identifier), + try + erlang:list_to_existing_atom(ModuleName) + catch + _:_ -> throw({error, ModuleName}) + end. + %%-------------------------------------------------------------------- %% @doc list the current path used to load the templates. %% @end %%-------------------------------------------------------------------- -path() -> +path() -> application:get_env(awesome, templates_path, ?DEFAULT_PATH). %%-------------------------------------------------------------------- %% @doc list all templates in default templates path. %% @end %%-------------------------------------------------------------------- -tree() -> +tree() -> Tree = tree(path()), lists:foldl(fun tree_filter/2, [], Tree). @@ -61,18 +93,18 @@ tree() -> %% @doc %% @end %%-------------------------------------------------------------------- -tree(Path) -> tree(Path, []). +tree(Path) -> tree(Path, [], Path). %%-------------------------------------------------------------------- %% @hidden %% @doc %% @end %%-------------------------------------------------------------------- -tree(Path, Buffer) -> +tree(Path, Buffer, Root) -> case file:list_dir(Path) of {ok, Files} -> - tree(Path, Files, Buffer); - {error, Error} -> + tree(Path, Files, Buffer, Root); + {error, Error} -> throw({error, Error}) end. @@ -81,51 +113,104 @@ tree(Path, Buffer) -> %% @doc %% @end %%-------------------------------------------------------------------- -tree(_, [], Buffer) -> Buffer; -tree(Path, [File|Files], Buffer) -> +tree(_, [], Buffer, Root) -> Buffer; +tree(Path, [File|Files], Buffer, Root) -> FilePath = filename:join([Path, File]), case filelib:is_dir(FilePath) of - true -> tree(FilePath, Buffer); - false -> tree(Path, Files, [{Path, FilePath}|Buffer]) + true -> + tree(FilePath, Buffer, Root); + false -> + F = filename:split(FilePath), + R = filename:split(Root), + S = lists:subtract(F, R), + RelativePath = filename:join(S), + tree(Path, Files, [{Path, FilePath, RelativePath}|Buffer], Root) end. %%-------------------------------------------------------------------- %% @hidden %% @doc paths filtering. +%% @see template_file/2 %% @end %%-------------------------------------------------------------------- -tree_filter({Key, Value}, Acc) -> - Origin = filename:split(Key), +-spec tree_filter({TemplatePath, TemplateFile, RelativePath}, Acc) -> Return when + TemplatePath :: string(), + TemplateFile :: string(), + RelativePath :: string(), + Acc :: [string(), ...], + Return :: Acc. +tree_filter({TemplatePath, _TemplateFile, RelativePath}, Acc) -> + Return = template_file(TemplatePath, RelativePath), + [Return|Acc]. + +%%-------------------------------------------------------------------- +%% +%%-------------------------------------------------------------------- +template_file(Filename) -> + TemplatePath = path(), + template_file(TemplatePath, Filename). + +%%-------------------------------------------------------------------- +%% @hidden +%% @doc this function takes the default path where the file should be +%% stored and the filename present in this path. If everything is fine, +%% it will output a map containing all information required to use +%% the file as template. +%% @end +%%-------------------------------------------------------------------- +-spec template_file(TemplatePath, TemplateFile) -> Return when + TemplatePath :: string(), + TemplateFile :: string(), + Return :: map(). +template_file(TemplatePath, TemplateFile) -> + case filelib:safe_relative_path(TemplateFile, TemplatePath) of + unsafe -> {error, unsafe}; + SafeFile -> + SafePath = filename:join(path(), SafeFile), + AbsolutePath = filename:absname(SafePath), + Filename = filename:basename(SafePath), + Identifier = template_file_identifier(TemplatePath, Filename), + case file:read_file(SafePath) of + {ok, Content} -> + #{ name => Identifier + , filename => Filename + , absolute_path => AbsolutePath + , relative_path => TemplatePath + , template => Content + }; + {error, Error} -> + throw(Error) + end + end. + +%%-------------------------------------------------------------------- +%% create a file identifier based on default path +%%-------------------------------------------------------------------- +template_file_identifier(TemplatePath, Filename) -> + Origin = filename:split(TemplatePath), Root = filename:split(path()), - Filename = lists:last(filename:split(Value)), - AbsolutePath = filename:absname(Value), Subtract = lists:subtract(Origin, Root), - Identifier = case Subtract of - [] -> filename:join(["/", Filename]); - _ -> filename:join(["/", Subtract, Filename]) - end, - {ok, Content} = file:read_file(AbsolutePath), - [#{ name => Identifier - , filename => Filename - , absolute_path => AbsolutePath - , relative_path => Key - , template => Content - }|Acc]. + template_file_identifier2(Filename, Subtract). + +template_file_identifier2(Filename, []) -> + filename:join(["/", Filename]); +template_file_identifier2(Filename, Subtract) -> + filename:join(["/", Subtract, Filename]). %%-------------------------------------------------------------------- %% @hidden %% @doc %% @end %%-------------------------------------------------------------------- -merl_template() -> +merl_template() -> merl_template(?DEFAULT_MERL_TEMPLATE). - + %%-------------------------------------------------------------------- %% @hidden %% @doc %% @end %%-------------------------------------------------------------------- -merl_template(TemplateFile) -> +merl_template(TemplateFile) -> case file:read_file(TemplateFile) of {ok, Content} -> Content; @@ -210,7 +295,7 @@ merl_subst_created_at(Template, #{ module_name := ModuleName } = Opts) -> true -> Original = ModuleName:created_at(), maps:get(created_at, Opts, Original); - false -> + false -> erlang:system_time() end, Term = merl:term(CreatedAt), diff --git a/src/gabarit_module.erl b/src/gabarit_module.erl new file mode 100644 index 0000000..2a561b8 --- /dev/null +++ b/src/gabarit_module.erl @@ -0,0 +1,19 @@ +-module(gabarit_module). +-compile(export_all). +-define(DEFAULT_PREFIX_FILE, "gabarit@"). +-define(DEFAULT_PREFIX_STRING, "gabarit$"). + +name(Prefix, Identifier) -> + ModuleName = string:concat(Prefix, Identifier), + try + {ok, erlang:list_to_existing_atom(ModuleName)} + catch + _:_ -> erlang:list_to_atom(ModuleName) + end. + +exist(Name) -> + try + _Module = erlang:list_to_existing_atom(Name) + catch + _:_ -> false + end. diff --git a/src/gabarit_store.erl b/src/gabarit_store.erl new file mode 100644 index 0000000..7b0796e --- /dev/null +++ b/src/gabarit_store.erl @@ -0,0 +1,19 @@ +%%%=================================================================== +%%% +%%%=================================================================== +-module(gabarit_store). +-behavior(gen_statem). +-compile(export_all). +-record(state, { store :: reference() }). + +callback_mode() -> state_functions. + +init(_Args) -> + Ets = ets:new(?MODULE, [private]), + {ok, unlocked, #state{ store = Ets }}. + +unlocked(_,_,Data) -> + {keep_state, Data}. + +locked(_,_,Data) -> + {keep_state, Data}. diff --git a/test/fixtures/mustache_templates/index.html b/test/fixtures/mustache_templates/index.html new file mode 100644 index 0000000..180110c --- /dev/null +++ b/test/fixtures/mustache_templates/index.html @@ -0,0 +1 @@ +{{content}} \ No newline at end of file diff --git a/test/gabarit_SUITE.erl b/test/gabarit_SUITE.erl new file mode 100644 index 0000000..b0f420d --- /dev/null +++ b/test/gabarit_SUITE.erl @@ -0,0 +1,130 @@ +%%%------------------------------------------------------------------- +%%% @author +%%% @copyright (C) 2023, +%%% @doc +%%% +%%% @end +%%% Created : 1 Aug 2023 by +%%%------------------------------------------------------------------- +-module(gabarit_SUITE). + +-compile(export_all). + +-include_lib("common_test/include/ct.hrl"). + +%%-------------------------------------------------------------------- +%% @spec suite() -> Info +%% Info = [tuple()] +%% @end +%%-------------------------------------------------------------------- +suite() -> + [{timetrap,{seconds,30}}]. + +%%-------------------------------------------------------------------- +%% @spec init_per_suite(Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + application:set_env(gabarit, templates_path, "../../fixtures/mustache_templates/"), + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_suite(Config0) -> term() | {save_config,Config1} +%% Config0 = Config1 = [tuple()] +%% @end +%%-------------------------------------------------------------------- +end_per_suite(_Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec init_per_group(GroupName, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% GroupName = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_group(_GroupName, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_group(GroupName, Config0) -> +%% term() | {save_config,Config1} +%% GroupName = atom() +%% Config0 = Config1 = [tuple()] +%% @end +%%-------------------------------------------------------------------- +end_per_group(_GroupName, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec init_per_testcase(TestCase, Config0) -> +%% Config1 | {skip,Reason} | {skip_and_save,Reason,Config1} +%% TestCase = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + Config. + +%%-------------------------------------------------------------------- +%% @spec end_per_testcase(TestCase, Config0) -> +%% term() | {save_config,Config1} | {fail,Reason} +%% TestCase = atom() +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, _Config) -> + ok. + +%%-------------------------------------------------------------------- +%% @spec groups() -> [Group] +%% Group = {GroupName,Properties,GroupsAndTestCases} +%% GroupName = atom() +%% Properties = [parallel | sequence | Shuffle | {RepeatType,N}] +%% GroupsAndTestCases = [Group | {group,GroupName} | TestCase] +%% TestCase = atom() +%% Shuffle = shuffle | {shuffle,{integer(),integer(),integer()}} +%% RepeatType = repeat | repeat_until_all_ok | repeat_until_all_fail | +%% repeat_until_any_ok | repeat_until_any_fail +%% N = integer() | forever +%% @end +%%-------------------------------------------------------------------- +groups() -> + []. + +%%-------------------------------------------------------------------- +%% @spec all() -> GroupsAndTestCases | {skip,Reason} +%% GroupsAndTestCases = [{group,GroupName} | TestCase] +%% GroupName = atom() +%% TestCase = atom() +%% Reason = term() +%% @end +%%-------------------------------------------------------------------- +all() -> + [common]. + +%%-------------------------------------------------------------------- +%% @spec TestCase() -> Info +%% Info = [tuple()] +%% @end +%%-------------------------------------------------------------------- +common() -> + []. + +%%-------------------------------------------------------------------- +%% @spec TestCase(Config0) -> +%% ok | exit() | {skip,Reason} | {comment,Comment} | +%% {save_config,Config1} | {skip_and_save,Reason,Config1} +%% Config0 = Config1 = [tuple()] +%% Reason = term() +%% Comment = term() +%% @end +%%-------------------------------------------------------------------- +common(_Config) -> + {ok, Module} = gabarit:new("index.html").