From 59a5092744c60e54d385d4ba9057a635e01d009d Mon Sep 17 00:00:00 2001 From: Nathan Ortega Date: Wed, 13 Dec 2023 21:13:31 -0500 Subject: [PATCH] feature/templating-support (#132) Added package extensions for Mustache.jl and OteraEngine.jl to provide better support for templating * add Dates compat section * removed Manifest.toml and added links to docs in both templating extensions * Added from_file keyword arg to both mustache() and otera() templating functions * Unified the api between extensions, data is passed without keywords as the first argument --- .gitignore | 2 + Manifest.toml | 190 ------------- Project.toml | 6 +- README.md | 100 +++++++ src/Oxygen.jl | 4 + src/extensions/load.jl | 19 ++ src/extensions/templating/mustache.jl | 78 ++++++ src/extensions/templating/oteraengine.jl | 72 +++++ src/extensions/templating/util.jl | 23 ++ test/Manifest.toml | 190 ------------- test/Project.toml | 10 +- test/content/mustache_template.txt | 5 + test/content/otera_template.html | 14 + test/content/otera_template_jl.html | 6 + test/content/otera_template_no_vars.html | 14 + test/content/otera_template_vars.html | 14 + test/runtests.jl | 6 +- test/templatingtests.jl | 342 +++++++++++++++++++++++ 18 files changed, 711 insertions(+), 384 deletions(-) delete mode 100644 Manifest.toml create mode 100644 src/extensions/load.jl create mode 100644 src/extensions/templating/mustache.jl create mode 100644 src/extensions/templating/oteraengine.jl create mode 100644 src/extensions/templating/util.jl delete mode 100644 test/Manifest.toml create mode 100644 test/content/mustache_template.txt create mode 100644 test/content/otera_template.html create mode 100644 test/content/otera_template_jl.html create mode 100644 test/content/otera_template_no_vars.html create mode 100644 test/content/otera_template_vars.html create mode 100644 test/templatingtests.jl diff --git a/.gitignore b/.gitignore index a0f0e538..c52079c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .vscode .DS_Store +Manifest.toml +test/Manifest.toml \ No newline at end of file diff --git a/Manifest.toml b/Manifest.toml deleted file mode 100644 index d28e6ba0..00000000 --- a/Manifest.toml +++ /dev/null @@ -1,190 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -[[Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" - -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[BitFlags]] -git-tree-sha1 = "43b1a4a8f797c1cddadf60499a8a077d4af2cd2d" -uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" -version = "0.1.7" - -[[CodecZlib]] -deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "9c209fb7536406834aa938fb149964b985de6c83" -uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.1" - -[[ConcurrentUtilities]] -deps = ["Serialization", "Sockets"] -git-tree-sha1 = "b306df2650947e9eb100ec125ff8c65ca2053d30" -uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" -version = "2.1.1" - -[[Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[HTTP]] -deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "69182f9a2d6add3736b7a06ab6416aafdeec2196" -uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.8.0" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[JLLWrappers]] -deps = ["Preferences"] -git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.4.1" - -[[JSON3]] -deps = ["Dates", "Mmap", "Parsers", "SnoopPrecompile", "StructTypes", "UUIDs"] -git-tree-sha1 = "84b10656a41ef564c39d2d477d7236966d2b5683" -uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.12.0" - -[[Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" - -[[Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[LoggingExtras]] -deps = ["Dates", "Logging"] -git-tree-sha1 = "cedb76b37bc5a6c702ade66be44f831fa23c681e" -uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" -version = "1.0.0" - -[[MIMEs]] -git-tree-sha1 = "65f28ad4b594aebe22157d6fac869786a255b7eb" -uuid = "6c6e2e6c-3030-632d-7369-2d6c69616d65" -version = "0.1.4" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[MbedTLS]] -deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "Random", "Sockets"] -git-tree-sha1 = "03a9b9718f5682ecb107ac9f7308991db4ce395b" -uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.1.7" - -[[MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" - -[[Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" - -[[MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" - -[[NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" - -[[OpenSSL]] -deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] -git-tree-sha1 = "7fb975217aea8f1bb360cf1dde70bad2530622d2" -uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" -version = "1.4.0" - -[[OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "6cc6366a14dbe47e5fc8f3cbe2816b1185ef5fc4" -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.8+0" - -[[Parsers]] -deps = ["Dates", "SnoopPrecompile"] -git-tree-sha1 = "478ac6c952fddd4399e71d4779797c538d0ff2bf" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.5.8" - -[[Preferences]] -deps = ["TOML"] -git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.3.0" - -[[Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[Random]] -deps = ["Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[RelocatableFolders]] -deps = ["SHA", "Scratch"] -git-tree-sha1 = "90bc7a7c96410424509e4263e277e43250c05691" -uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" -version = "1.0.0" - -[[SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[Scratch]] -deps = ["Dates"] -git-tree-sha1 = "30449ee12237627992a99d5e30ae63e4d78cd24a" -uuid = "6c6a2e73-6563-6170-7368-637461726353" -version = "1.2.0" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[SimpleBufferStream]] -git-tree-sha1 = "874e8867b33a00e784c8a7e4b60afe9e037b74e1" -uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" -version = "1.1.0" - -[[SnoopPrecompile]] -deps = ["Preferences"] -git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c" -uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c" -version = "1.0.3" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[StructTypes]] -deps = ["Dates", "UUIDs"] -git-tree-sha1 = "ca4bccb03acf9faaf4137a9abc1881ed1841aa70" -uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" -version = "1.10.0" - -[[TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" - -[[Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[TranscodingStreams]] -deps = ["Random", "Test"] -git-tree-sha1 = "9a6ae7ed916312b41236fcef7e0af564ef934769" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.9.13" - -[[URIs]] -git-tree-sha1 = "074f993b0ca030848b897beff716d93aca60f06a" -uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.4.2" - -[[UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" - -[[Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" diff --git a/Project.toml b/Project.toml index 1650adff..66b115b5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Oxygen" uuid = "df9a0d86-3283-4920-82dc-4555fc0d1d8b" authors = ["Nathan Ortega "] -version = "1.2" +version = "1.2.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -9,11 +9,15 @@ HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" MIMEs = "6c6e2e6c-3030-632d-7369-2d6c69616d65" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" [compat] +Dates = "^1" HTTP = "^1" JSON3 = "^1.9" MIMEs = "^0.1.4" RelocatableFolders = "^1" +Requires = "^1" +Sockets = "^1" julia = "^1.6" diff --git a/README.md b/README.md index d3b56337..6303c9a9 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Breathe easy knowing you can quickly spin up a web server with abstractions you' - Built-in Cron Scheduling (on endpoints & functions) - Middleware chaining (at the application, router, and route levels) - Static & Dynamic file hosting +- Templating Support - Route tagging - Repeat tasks @@ -363,6 +364,105 @@ end serve() ``` + +## Templating + +Rather than building an internal engine for templating or adding additional dependencies, Oxygen +provides two package extensions and helper functions to support `Mustache.jl` and `OteraEngine.jl` templates. + +Oxygen provides a simple wrapper api around both packages that makes it easy to render templates from strings, +templates, and files. This api returns a `render` function which takes accepts input arguments to fill out the +template. + +In all scenarios, the rendered template is formatted inside a single HTTP.Response object ready to get served by the api. +By default, the mime types are auto-detected either by looking at the content of the template or the extension name on the file. +If you know the mime type you can pass it directly through the `mime_type` keyword argument to skip the detection process. + +### Mustache Templating +Please take a look at the [Mustache.jl](https://jverzani.github.io/Mustache.jl/dev/) documentation to learn the full capabilities of the package + +Example 1: Rendering a Mustache Template from a File + +```julia +using Oxygen + +# Load the Mustache template from a file and create a render function +render = mustache("./templates/greeting.txt", from_file=false) + +@get "/mustache/file" function() + data = Dict("name" => "Chris") + return render(data) # This will return an HTML.Response with the rendered template +end +``` + +Example 2: Specifying MIME Type for a plain string Mustache Template +```julia +using Oxygen + +# Define a Mustache template (both plain strings and mustache templates are supported) +template_str = "Hello, {{name}}!" + +# Create a render function, specifying the MIME type as text/plain +render = mustache(template_str, mime_type="text/plain") # mime_type keyword arg is optional + +@get "/plain/text" function() + data = Dict("name" => "Chris") + return render(data) # This will return a plain text response with the rendered template +end +``` + +### Otera Templating +Please take a look at the [OteraEngine.jl](https://mommawatasu.github.io/OteraEngine.jl/dev/tutorial/#API) documentation to learn the full capabilities of the package + +Example 1: Rendering an Otera Template with Logic and Loops + +```julia +using Oxygen + +# Define an Otera template +template_str = """ + + {{ title }} + + {% for name in names %} + Hello {{ name }}
+ {% end %} + + +""" + +# Create a render function for the Otera template +render = otera(template_str) + +@get "/otera/loop" function() + data = Dict("title" => "Greetings", "names" => ["Alice", "Bob", "Chris"]) + return render(data) # This will return an HTML.Response with the rendered template +end +``` + +In this example, an Otera template is defined with a for-loop that iterates over a list of names, greeting each name. + +Example 2: Running Julia Code in Otera Template +```julia +using Oxygen + +# Define an Otera template with embedded Julia code +template_str = """ +The square of {{ number }} is {< number^2 >}. +""" + +# Create a render function for the Otera template +render = otera(template_str) + +@get "/otera/square" function() + data = Dict("number" => 5) + return render(data) # This will return an HTML.Response with the rendered template +end + +``` + +In this example, an Otera template is defined with embedded Julia code that calculates the square of a given number. + ## Mounting Static Files You can mount static files using this handy function which recursively searches a folder for files and mounts everything. All files are diff --git a/src/Oxygen.jl b/src/Oxygen.jl index 589d0ebb..22df918b 100644 --- a/src/Oxygen.jl +++ b/src/Oxygen.jl @@ -15,4 +15,8 @@ export @get, @post, @put, @patch, @delete, @route, @cron, configdocs, mergeschema, setschema, getschema, router, enabledocs, disabledocs, isdocsenabled, starttasks, stoptasks, resetstate, startcronjobs, stopcronjobs + +# Load any optional extensions +include("extensions/load.jl"); + end \ No newline at end of file diff --git a/src/extensions/load.jl b/src/extensions/load.jl new file mode 100644 index 00000000..f8e7b469 --- /dev/null +++ b/src/extensions/load.jl @@ -0,0 +1,19 @@ +using Requires + +function __init__() + + + ################################################################ + # Templating Extensions # + ################################################################ + + @require Mustache="ffc61752-8dc7-55ee-8c37-f3e9cdd09e70" begin + include("templating/mustache.jl"); using .MustacheTemplating + export mustache + end + + @require OteraEngine="b2d7f28f-acd6-4007-8b26-bc27716e5513" begin + include("templating/oteraengine.jl"); using .OteraEngineTemplating + export otera + end +end diff --git a/src/extensions/templating/mustache.jl b/src/extensions/templating/mustache.jl new file mode 100644 index 00000000..25e51861 --- /dev/null +++ b/src/extensions/templating/mustache.jl @@ -0,0 +1,78 @@ +module MustacheTemplating +using MIMEs +using Mustache +include("util.jl"); using .TemplatingUtil + +export mustache + + +""" + mustache(template::String; kwargs...) + +Create a function that renders a Mustache `template` string with the provided `kwargs`. +If `template` is a file path, it reads the file content as the template string. +Returns a function that takes a dictionary `data`, optional `status`, and `headers`, and +returns an HTTP Response object with the rendered content. + +To get more info read the docs here: https://github.com/jverzani/Mustache.jl +""" +function mustache(template::String; mime_type=nothing, from_file=false, kwargs...) + mime_is_known = !isnothing(mime_type) + + # Case 1: a path to a file was passed + if from_file + if mime_is_known + return mustache(open(template); mime_type=mime_type, kwargs...) + else + # deterime the mime type based on the extension type + content_type = mime_from_path(template, MIME"application/octet-stream"()) |> contenttype_from_mime + return mustache(open(template); mime_type=content_type, kwargs...) + end + end + + # Case 2: A string template was passed directly + function(data::AbstractDict = Dict(); status=200, headers=[]) + content = Mustache.render(template, data; kwargs...) + resp_headers = mime_is_known ? [["Content-Type" => mime_type]; headers] : headers + response(content, status, resp_headers; detect=!mime_is_known) + end +end + +""" + mustache(tokens::Mustache.MustacheTokens; kwargs...) + +Create a function that renders a Mustache template defined by `tokens` with the provided `kwargs`. +Returns a function that takes a dictionary `data`, optional `status`, and `headers`, and +returns an HTTP Response object with the rendered content. + +To get more info read the docs here: https://github.com/jverzani/Mustache.jl +""" +function mustache(tokens::Mustache.MustacheTokens; mime_type=nothing, kwargs...) + mime_is_known = !isnothing(mime_type) + return function(data::AbstractDict = Dict(); status=200, headers=[]) + content = Mustache.render(tokens, data; kwargs...) + resp_headers = mime_is_known ? [["Content-Type" => mime_type]; headers] : headers + response(content, status, resp_headers; detect=!mime_is_known) + end +end + +""" + mustache(file::IO; kwargs...) + +Create a function that renders a Mustache template from a file `file` with the provided `kwargs`. +Returns a function that takes a dictionary `data`, optional `status`, and `headers`, and +returns an HTTP Response object with the rendered content. + +To get more info read the docs here: https://github.com/jverzani/Mustache.jl +""" +function mustache(file::IO; mime_type=nothing, kwargs...) + template = read(file, String) + mime_is_known = !isnothing(mime_type) + return function(data::AbstractDict = Dict(); status=200, headers=[]) + content = Mustache.render(template, data; kwargs...) + resp_headers = mime_is_known ? [["Content-Type" => mime_type]; headers] : headers + response(content, status, resp_headers; detect=!mime_is_known) + end +end + +end \ No newline at end of file diff --git a/src/extensions/templating/oteraengine.jl b/src/extensions/templating/oteraengine.jl new file mode 100644 index 00000000..5b115e73 --- /dev/null +++ b/src/extensions/templating/oteraengine.jl @@ -0,0 +1,72 @@ +module OteraEngineTemplating +using MIMEs +using OteraEngine +include("util.jl"); using .TemplatingUtil + +export otera + +""" + otera(template::String; kwargs...) + +Create a function that renders an Otera `template` string with the provided `kwargs`. +If `template` is a file path, it reads the file content as the template string. +Returns a function that takes a dictionary `data` (default is an empty dictionary), +optional `status`, `headers`, and `template_kwargs`, and returns an HTTP Response object +with the rendered content. + +To get more info read the docs here: https://github.com/MommaWatasu/OteraEngine.jl +""" +function otera(template::String; mime_type=nothing, from_file=false, kwargs...) + mime_is_known = !isnothing(mime_type) + + # Case 1: a path to a file was passed + if from_file + if mime_is_known + return otera(open(template); mime_type=mime_type, kwargs...) + else + # deterime the mime type based on the extension type + content_type = mime_from_path(template, MIME"application/octet-stream"()) |> contenttype_from_mime + return otera(open(template); mime_type=content_type, kwargs...) + end + end + + # Case 2: A string template was passed directly + tmp = Template(template, path=from_file; kwargs...) + return function(data = nothing; status=200, headers=[], template_kwargs...) + combined_kwargs = Dict{Symbol, Any}(template_kwargs) + if data !== nothing + combined_kwargs[:init] = data + end + content = tmp(; combined_kwargs...) + resp_headers = mime_is_known ? [["Content-Type" => mime_type]; headers] : headers + response(content, status, resp_headers; detect=!mime_is_known) + end +end + + +""" + otera(file::IO; kwargs...) + +Create a function that renders an Otera template from a file `file` with the provided `kwargs`. +Returns a function that takes a dictionary `data`, optional `status`, `headers`, and `template_kwargs`, +and returns an HTTP Response object with the rendered content. + +To get more info read the docs here: https://github.com/MommaWatasu/OteraEngine.jl +""" +function otera(file::IO; mime_type=nothing, kwargs...) + template = read(file, String) + mime_is_known = !isnothing(mime_type) + tmp = Template(template, path=false; kwargs...) + + return function(data = nothing; status=200, headers=[], template_kwargs...) + combined_kwargs = Dict{Symbol, Any}(template_kwargs) + if data !== nothing + combined_kwargs[:init] = data + end + content = tmp(; combined_kwargs...) + resp_headers = mime_is_known ? [["Content-Type" => mime_type]; headers] : headers + response(content, status, resp_headers; detect=!mime_is_known) + end +end + +end \ No newline at end of file diff --git a/src/extensions/templating/util.jl b/src/extensions/templating/util.jl new file mode 100644 index 00000000..2701603e --- /dev/null +++ b/src/extensions/templating/util.jl @@ -0,0 +1,23 @@ +module TemplatingUtil +using HTTP +using MIMEs + +export response + +""" + response(content::String, status=200, headers=[]) :: HTTP.Response + +Convert a template string `content` into a valid HTTP Response object. +The content type header is automatically generated based on the content's mimetype +- `content`: The string content to be included in the HTTP response body. +- `status`: The HTTP status code (default is 200). +- `headers`: Additional HTTP headers to include (default is an empty array). + +Returns an `HTTP.Response` object with the specified content, status, and headers. +""" +function response(content::String, status=200, headers=[]; detect=true) :: HTTP.Response + response_headers = detect ? ["Content-Type" => HTTP.sniff(content)] : [] + return HTTP.Response(status, [response_headers; headers;], content) +end + +end \ No newline at end of file diff --git a/test/Manifest.toml b/test/Manifest.toml deleted file mode 100644 index d28e6ba0..00000000 --- a/test/Manifest.toml +++ /dev/null @@ -1,190 +0,0 @@ -# This file is machine-generated - editing it directly is not advised - -[[Artifacts]] -uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" - -[[Base64]] -uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" - -[[BitFlags]] -git-tree-sha1 = "43b1a4a8f797c1cddadf60499a8a077d4af2cd2d" -uuid = "d1d4a3ce-64b1-5f1a-9ba4-7e7e69966f35" -version = "0.1.7" - -[[CodecZlib]] -deps = ["TranscodingStreams", "Zlib_jll"] -git-tree-sha1 = "9c209fb7536406834aa938fb149964b985de6c83" -uuid = "944b1d66-785c-5afd-91f1-9de20f533193" -version = "0.7.1" - -[[ConcurrentUtilities]] -deps = ["Serialization", "Sockets"] -git-tree-sha1 = "b306df2650947e9eb100ec125ff8c65ca2053d30" -uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" -version = "2.1.1" - -[[Dates]] -deps = ["Printf"] -uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" - -[[HTTP]] -deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] -git-tree-sha1 = "69182f9a2d6add3736b7a06ab6416aafdeec2196" -uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" -version = "1.8.0" - -[[InteractiveUtils]] -deps = ["Markdown"] -uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" - -[[JLLWrappers]] -deps = ["Preferences"] -git-tree-sha1 = "abc9885a7ca2052a736a600f7fa66209f96506e1" -uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" -version = "1.4.1" - -[[JSON3]] -deps = ["Dates", "Mmap", "Parsers", "SnoopPrecompile", "StructTypes", "UUIDs"] -git-tree-sha1 = "84b10656a41ef564c39d2d477d7236966d2b5683" -uuid = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" -version = "1.12.0" - -[[Libdl]] -uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" - -[[Logging]] -uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" - -[[LoggingExtras]] -deps = ["Dates", "Logging"] -git-tree-sha1 = "cedb76b37bc5a6c702ade66be44f831fa23c681e" -uuid = "e6f89c97-d47a-5376-807f-9c37f3926c36" -version = "1.0.0" - -[[MIMEs]] -git-tree-sha1 = "65f28ad4b594aebe22157d6fac869786a255b7eb" -uuid = "6c6e2e6c-3030-632d-7369-2d6c69616d65" -version = "0.1.4" - -[[Markdown]] -deps = ["Base64"] -uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" - -[[MbedTLS]] -deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "Random", "Sockets"] -git-tree-sha1 = "03a9b9718f5682ecb107ac9f7308991db4ce395b" -uuid = "739be429-bea8-5141-9913-cc70e7f3736d" -version = "1.1.7" - -[[MbedTLS_jll]] -deps = ["Artifacts", "Libdl"] -uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" - -[[Mmap]] -uuid = "a63ad114-7e13-5084-954f-fe012c677804" - -[[MozillaCACerts_jll]] -uuid = "14a3606d-f60d-562e-9121-12d972cd8159" - -[[NetworkOptions]] -uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" - -[[OpenSSL]] -deps = ["BitFlags", "Dates", "MozillaCACerts_jll", "OpenSSL_jll", "Sockets"] -git-tree-sha1 = "7fb975217aea8f1bb360cf1dde70bad2530622d2" -uuid = "4d8831e6-92b7-49fb-bdf8-b643e874388c" -version = "1.4.0" - -[[OpenSSL_jll]] -deps = ["Artifacts", "JLLWrappers", "Libdl"] -git-tree-sha1 = "6cc6366a14dbe47e5fc8f3cbe2816b1185ef5fc4" -uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.0.8+0" - -[[Parsers]] -deps = ["Dates", "SnoopPrecompile"] -git-tree-sha1 = "478ac6c952fddd4399e71d4779797c538d0ff2bf" -uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" -version = "2.5.8" - -[[Preferences]] -deps = ["TOML"] -git-tree-sha1 = "47e5f437cc0e7ef2ce8406ce1e7e24d44915f88d" -uuid = "21216c6a-2e73-6563-6e65-726566657250" -version = "1.3.0" - -[[Printf]] -deps = ["Unicode"] -uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" - -[[Random]] -deps = ["Serialization"] -uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[[RelocatableFolders]] -deps = ["SHA", "Scratch"] -git-tree-sha1 = "90bc7a7c96410424509e4263e277e43250c05691" -uuid = "05181044-ff0b-4ac5-8273-598c1e38db00" -version = "1.0.0" - -[[SHA]] -uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" - -[[Scratch]] -deps = ["Dates"] -git-tree-sha1 = "30449ee12237627992a99d5e30ae63e4d78cd24a" -uuid = "6c6a2e73-6563-6170-7368-637461726353" -version = "1.2.0" - -[[Serialization]] -uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" - -[[SimpleBufferStream]] -git-tree-sha1 = "874e8867b33a00e784c8a7e4b60afe9e037b74e1" -uuid = "777ac1f9-54b0-4bf8-805c-2214025038e7" -version = "1.1.0" - -[[SnoopPrecompile]] -deps = ["Preferences"] -git-tree-sha1 = "e760a70afdcd461cf01a575947738d359234665c" -uuid = "66db9d55-30c0-4569-8b51-7e840670fc0c" -version = "1.0.3" - -[[Sockets]] -uuid = "6462fe0b-24de-5631-8697-dd941f90decc" - -[[StructTypes]] -deps = ["Dates", "UUIDs"] -git-tree-sha1 = "ca4bccb03acf9faaf4137a9abc1881ed1841aa70" -uuid = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" -version = "1.10.0" - -[[TOML]] -deps = ["Dates"] -uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" - -[[Test]] -deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] -uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" - -[[TranscodingStreams]] -deps = ["Random", "Test"] -git-tree-sha1 = "9a6ae7ed916312b41236fcef7e0af564ef934769" -uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" -version = "0.9.13" - -[[URIs]] -git-tree-sha1 = "074f993b0ca030848b897beff716d93aca60f06a" -uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" -version = "1.4.2" - -[[UUIDs]] -deps = ["Random", "SHA"] -uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" - -[[Unicode]] -uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" - -[[Zlib_jll]] -deps = ["Libdl"] -uuid = "83775a58-1f1d-513f-b197-d71354ab007a" diff --git a/test/Project.toml b/test/Project.toml index 5546add9..eb1ad71e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -3,13 +3,21 @@ Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" MIMEs = "6c6e2e6c-3030-632d-7369-2d6c69616d65" +Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70" +OteraEngine = "b2d7f28f-acd6-4007-8b26-bc27716e5513" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" +Requires = "ae029012-a4dd-5104-9daa-d747884805df" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] +Dates = "^1" HTTP = "^1" JSON3 = "^1.9" MIMEs = "^0.1.4" -julia = "^1.6.6" +RelocatableFolders = "^1" +Requires = "^1" +Sockets = "^1" +julia = "^1.6" \ No newline at end of file diff --git a/test/content/mustache_template.txt b/test/content/mustache_template.txt new file mode 100644 index 00000000..381ad7ea --- /dev/null +++ b/test/content/mustache_template.txt @@ -0,0 +1,5 @@ +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} \ No newline at end of file diff --git a/test/content/otera_template.html b/test/content/otera_template.html new file mode 100644 index 00000000..2c990da8 --- /dev/null +++ b/test/content/otera_template.html @@ -0,0 +1,14 @@ + + MyPage + + {% if name=="watasu" %} + your name is {{ name }}, right? + {% end %} + {% for i in 1 : 10 %} + Hello {{i}} + {% end %} + {% if age == 15 %} + and your age is {{ age }}. + {% end %} + + \ No newline at end of file diff --git a/test/content/otera_template_jl.html b/test/content/otera_template_jl.html new file mode 100644 index 00000000..a40ce29c --- /dev/null +++ b/test/content/otera_template_jl.html @@ -0,0 +1,6 @@ + + MyPage + +

Hello {}!

+ + \ No newline at end of file diff --git a/test/content/otera_template_no_vars.html b/test/content/otera_template_no_vars.html new file mode 100644 index 00000000..7a89daa7 --- /dev/null +++ b/test/content/otera_template_no_vars.html @@ -0,0 +1,14 @@ + + MyPage + + {% set name = "watasu" %} + {% set age = 15 %} + {% if name == "watasu" %} + your name is {{ name }}, right? + {% end %} + {% for i in 1 : 10 %} + Hello {{i}} + {% end %} + and your age is {{ age }}. + + \ No newline at end of file diff --git a/test/content/otera_template_vars.html b/test/content/otera_template_vars.html new file mode 100644 index 00000000..4ba5ddfe --- /dev/null +++ b/test/content/otera_template_vars.html @@ -0,0 +1,14 @@ + + MyPage + + {% if name == "watasu" %} + your name is {{ name }}, right? + {% end %} + {% for i in 1 : 10 %} + Hello {{i}} + {% end %} + {% with age = 15 %} + and your age is {{ age }}. + {% end %} + + \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 8392897f..9532f432 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,12 +6,14 @@ using StructTypes using Sockets using Dates +include("../src/Oxygen.jl") +using .Oxygen + +include("templatingtests.jl") include("routingfunctionstests.jl") include("bodyparsertests.jl") include("crontests.jl") -include("../src/Oxygen.jl") -using .Oxygen struct Person name::String diff --git a/test/templatingtests.jl b/test/templatingtests.jl new file mode 100644 index 00000000..a6b9dbdc --- /dev/null +++ b/test/templatingtests.jl @@ -0,0 +1,342 @@ +module TemplatingTests +using MIMEs +using Test +using HTTP +using Mustache +using OteraEngine + +include("../src/Oxygen.jl") +using .Oxygen + +# ensure the init is called so we can load the extensions +Oxygen.__init__() + + +function clean_output(result::String) + # Replace carriage returns followed by line feeds (\r\n) with a single newline (\n) + tmp = replace(result, "\r\n" => "\n") + + # Replace sequences of newline followed by spaces (\n\s*) with a single newline (\n) + tmp = replace(tmp, r"\n\s*\n" => "\n") + + return tmp +end + + +function remove_trailing_newline(s::String)::String + if !isempty(s) && last(s) == '\n' + return s[1:end-1] + end + return s +end + + +data = Dict( + "name" => "Chris", + "value" => 10000, + "taxed_value" => 10000 - (10000 * 0.4), + "in_ca" => true +) + +mustache_template = mt""" +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +""" + +mustache_template_str = """ +Hello {{name}} +You have just won {{value}} dollars! +{{#in_ca}} +Well, {{taxed_value}} dollars, after taxes. +{{/in_ca}} +""" + +expected_output = """ +Hello Chris +You have just won 10000 dollars! +Well, 6000.0 dollars, after taxes. +""" + +@testset "mustache() from string no args " begin + plain_temp = "Hello World!" + render = mustache(plain_temp, mime_type="text/plain") + response = render() + @test response.body |> String |> clean_output == plain_temp +end + +@testset "mustache() from string tests " begin + render = mustache(mustache_template_str, mime_type="text/plain") + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + +@testset "mustache() from string tests w/ content type" begin + render = mustache(mustache_template_str) + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + + + +@testset "mustache() from file no content type" begin + render = mustache("./content/mustache_template.txt", from_file=true) + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + +@testset "mustache() from file w/ content type" begin + render = mustache("./content/mustache_template.txt", mime_type="text/plain", from_file=true) + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + + + +@testset "mustache() from file with no content type" begin + render = mustache(open("./content/mustache_template.txt")) + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + +@testset "mustache() from file with content type" begin + render = mustache(open("./content/mustache_template.txt"), mime_type="text/plain") + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + + + +@testset "mustache() from template" begin + render = mustache(mustache_template) + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + +@testset "mustache() from template with content type" begin + render = mustache(mustache_template, mime_type="text/plain") + response = render(data) + @test response.body |> String |> clean_output == expected_output +end + + +@testset "mustache api tests" begin + + mus_str = mustache(mustache_template_str) + mus_tpl = mustache(mustache_template) + mus_file = mustache("./content/mustache_template.txt", from_file=true) + + @get "/mustache/string" function() + return mus_str(data) + end + + @get "/mustache/template" function() + return mus_tpl(data) + end + + @get "/mustache/file" function() + return mus_file(data) + end + + r = internalrequest(HTTP.Request("GET", "/mustache/string")) + @test r.status == 200 + @test r.body |> String |> clean_output == expected_output + + r = internalrequest(HTTP.Request("GET", "/mustache/template")) + @test r.status == 200 + @test r.body |> String |> clean_output == expected_output + + r = internalrequest(HTTP.Request("GET", "/mustache/file")) + @test r.status == 200 + @test r.body |> String |> clean_output == expected_output + +end + + + +@testset "otera() from string" begin + + template = """ + + MyPage + + {% if name=="watasu" %} + your name is {{ name }}, right? + {% end %} + {% for i in 1 : 10 %} + Hello {{i}} + {% end %} + {% if age == 15 %} + and your age is {{ age }}. + {% end %} + + + """ |> remove_trailing_newline + + expected_output = """ + + MyPage + + your name is watasu, right? + Hello 1 + Hello 2 + Hello 3 + Hello 4 + Hello 5 + Hello 6 + Hello 7 + Hello 8 + Hello 9 + Hello 10 + and your age is 15. + + + """ |> remove_trailing_newline + + # detect content type + data = Dict("name" => "watasu", "age" => 15) + render = otera(template) + result = render(data) + @test result.body |> String |> clean_output == expected_output + + # with explicit content type + data = Dict("name" => "watasu", "age" => 15) + render = otera(template; mime_type="text/html") + result = render(data) + @test result.body |> String |> clean_output == expected_output + +end + + +@testset "otera() from template file" begin + + expected_output = """ + + MyPage + + your name is watasu, right? + Hello 1 + Hello 2 + Hello 3 + Hello 4 + Hello 5 + Hello 6 + Hello 7 + Hello 8 + Hello 9 + Hello 10 + and your age is 15. + + + """ |> remove_trailing_newline + + data = Dict("name" => "watasu", "age" => 15) + + render = otera("./content/otera_template.html", from_file=true) + result = render(data) + @test result.body |> String |> clean_output == expected_output + + # with explicit content type + render = otera(open("./content/otera_template.html"); mime_type="text/html") + result = render(data) + @test result.body |> String |> clean_output == expected_output +end + + +@testset "otera() from template file with no args" begin + + expected_output = """ + + MyPage + + your name is watasu, right? + Hello 1 + Hello 2 + Hello 3 + Hello 4 + Hello 5 + Hello 6 + Hello 7 + Hello 8 + Hello 9 + Hello 10 + and your age is 15. + + + """ |> remove_trailing_newline + + render = otera("./content/otera_template_no_vars.html", from_file=true) + result = render() + @test result.body |> String |> clean_output == expected_output + + render = otera("./content/otera_template_no_vars.html", mime_type="text/html", from_file=true) + result = render() + @test result.body |> String |> clean_output == expected_output +end + + +@testset "otera() from template file with jl init data" begin + + expected_output = """ + + MyPage + +

Hello World!

+ + + """ |> remove_trailing_newline + + render = otera("./content/otera_template_jl.html", from_file=true) + result = render(Dict("name" => "World")) + @test result.body |> String |> clean_output == expected_output +end + +@testset "otera() running julia code in template" begin + template = "{< 3 ^ 3 >}. Hello {{ name }}!" + expected_output = "27. Hello world!" + render = otera(template) + result = render(Dict("name"=>"world")) + @test result.body |> String |> clean_output == expected_output + + template = """ + + Jinja Test Page + + Hello, {}! + + + """ |> remove_trailing_newline + + expected_output = """ + + Jinja Test Page + + Hello, world! + + + """ |> remove_trailing_newline + + render = otera(template) + result = render(Dict("name"=>"world")) + @test result.body |> String |> clean_output == expected_output +end + +@testset "otera() combined tmp_init & init test" begin + template = "{< parse(Int, value) ^ 3 >}. Hello {{name}}!" + expected_output = "27. Hello World!" + render = otera(template) + result = render(Dict("name" => "World", "value" => "3")) +end + +@testset "otera() combined tmp_init & init test with content type" begin + template = "{< parse(Int, value) ^ 3 >}. Hello {{name}}!" + expected_output = "27. Hello World!" + render = otera(template, mime_type = "text/plain") + result = render(Dict("name" => "World", "value" => "3")) +end + + + +end \ No newline at end of file