├── test ├── .gitkeep ├── views │ ├── argsencoded1.jl.html │ ├── argsencoded3.jl.html │ ├── argsencoded2.jl.html │ ├── view.jl.md │ ├── layout.jl.html │ ├── view-vars.jl.md │ ├── layoutargsencoding.jl.html │ ├── outputscripttags.jl.html │ └── layoutscripttags.jl.html ├── cors │ ├── Project.toml │ └── cors.jl ├── fileuploads │ ├── Project.toml │ └── test.jl ├── runtests.jl ├── tests_SVG_API.jl ├── tests_hello.jl ├── formview.jl.html ├── tests_router.jl ├── tests_assets_rendering.jl ├── Project.toml ├── tests_advanced_select_rendering.jl ├── tests_options_request.jl ├── tests_Sessions.jl ├── tests_arguments_parsing.jl ├── tests_responses.jl ├── tests_json_rendering.jl ├── tests_js_rendering.jl ├── tests_views_rendering.jl ├── tests_1_config.jl ├── tests_views_control_flow_rendering.jl ├── tests_headers.jl ├── tests_json_payload.jl ├── tests_cache.jl ├── tests_zz_fullstack_app.jl ├── tests_peer_info.jl ├── tests_post_data.jl ├── tests_AppServer.jl ├── tests_routing.jl ├── tests_advanced_html_rendering.jl ├── tests_Assets.jl ├── tests_vars_rendering.jl ├── tests_output_script_tags.jl ├── tests_query_special_chars.jl ├── tests_markdown_rendering.jl ├── tests_arguments_encoding.jl ├── tests_z_HEAD_requests.jl ├── tests_html_rendering.jl ├── tests_basic_rendering.jl └── tests_genie_generators.jl ├── docs ├── src │ ├── .gitkeep │ ├── tutorials │ │ ├── 91--Deploying_Genie_Docker_Apps_on_Heroku.md │ │ ├── 14--The_Secrets_File.md │ │ ├── 2--Installing_Genie.md │ │ ├── 15--The_Lib_Folder.md │ │ ├── 11--Managing_External_Packages.md │ │ ├── 8--Handling_File_Uploads.md │ │ ├── 80--Force_Compiling_Routes.md │ │ ├── 6--Working_with_POST_Payloads.md │ │ ├── 1--Overview.md │ │ ├── 7--Using_JSON_Payloads.md │ │ ├── 5--Handling_Query_Params.md │ │ ├── 9--Publishing_Your_Julia_Code_Online_With_Genie_Apps.md │ │ ├── 13--Initializers.md │ │ ├── 10--Loading_Genie_Apps.md │ │ ├── 16--Using_Genie_With_Docker.md │ │ ├── 3--Getting_Started.md │ │ └── 4--Developing_Web_Services.md │ ├── api │ │ ├── app.md │ │ ├── httputils.md │ │ ├── flash.md │ │ ├── deploy-docker.md │ │ ├── cache.md │ │ ├── encryption.md │ │ ├── commands.md │ │ ├── plugins.md │ │ ├── cookies.md │ │ ├── headers.md │ │ ├── deploy-heroku.md │ │ ├── renderer-js.md │ │ ├── filetemplates.md │ │ ├── renderer-json.md │ │ ├── util.md │ │ ├── configuration.md │ │ ├── exceptions.md │ │ ├── sessions.md │ │ ├── assets.md │ │ ├── responses.md │ │ ├── appserver.md │ │ ├── toolbox.md │ │ ├── inflector.md │ │ ├── input.md │ │ ├── requests.md │ │ ├── renderer.md │ │ ├── genie.md │ │ ├── webchannels.md │ │ ├── generator.md │ │ ├── index.md │ │ ├── renderer-html.md │ │ └── router.md │ ├── index.md │ └── guides │ │ ├── Simple_API_backend.md │ │ ├── Interactive_environment.md │ │ └── Frontend_assets.md ├── _todo_ │ ├── Genie_Caching.md │ ├── Genie_Plugins.md │ ├── Custom_Error_Pages.md │ ├── Custom_Responses.md │ ├── Genie_App_REPL.md │ ├── Handling_Requests.md │ ├── Handling_Responses.md │ ├── Redirect_Responses.md │ ├── Testing_Genie_Apps.md │ ├── Using_Environments.md │ ├── Configuration_Options.md │ ├── Developing_MVC_Web_Apps.md │ ├── Flax_Templating_Engine.md │ ├── Important_Genie_Files.md │ ├── Rendering_HTML_Layouts.md │ ├── Rendering_JSON_Layouts.md │ ├── SearchLight_ORM_Support.md │ ├── Sending_HTML_Responses.md │ ├── Sending_JSON_Responses.md │ ├── Sessions_And_Cookies.md │ ├── Working_With_WebSockets.md │ ├── Developing_Genie_Plugins.md │ ├── Encrypting_Data_In_Genie.md │ ├── Files_And_Folders_Structure.md │ ├── Options_Preflight_Responses.md │ ├── Plugins_AutoReload_Plugin.md │ ├── Rendering_Markdown_Layouts.md │ ├── Developing_Full_Stack_Genie_Apps.md │ ├── Plugins_Authentication_Plugin.md │ ├── Reloading_Code_In_Development.md │ ├── Rendering_Exception_Responses.md │ ├── Setting_Up_The_Asset_Pipeline.md │ ├── Accelerated_Development_With_Generators.md │ ├── Data_Validation_With_SearchLight_Validators.md │ ├── Database_Versionsing_With_SearchLight_Migrations.md │ ├── Validating_Data_With_SearchLight_Model_Validators.md │ ├── Mastering_SearchLight_Models.md │ └── Mastering_Genie_Controllers.md ├── _config.yml ├── content │ └── img │ │ ├── genie.gif │ │ └── genie_logo.png ├── Project.toml └── mkdocs.yml ├── files ├── new_app │ ├── bin │ │ └── .gitkeep │ ├── log │ │ └── .gitkeep │ ├── src │ │ └── .gitkeep │ ├── test │ │ └── .gitkeep │ ├── db │ │ ├── seeds │ │ │ └── .gitkeep │ │ ├── migrations │ │ │ └── .gitkeep │ │ └── connection.yml │ ├── plugins │ │ └── .gitkeep │ ├── public │ │ ├── .gitkeep │ │ ├── favicon.ico │ │ ├── robots.txt │ │ ├── img │ │ │ └── genie │ │ │ │ ├── docs.png │ │ │ │ ├── genie.png │ │ │ │ ├── community.png │ │ │ │ ├── genie-sad.png │ │ │ │ └── contribute-2.png │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── js │ │ │ └── genie │ │ │ │ └── static.js │ │ ├── css │ │ │ └── genie │ │ │ │ └── prism.css │ │ ├── welcome.html │ │ ├── error-xxx.html │ │ ├── error-404.html │ │ └── error-500.html │ ├── app │ │ ├── resources │ │ │ └── .gitkeep │ │ ├── layouts │ │ │ └── app.jl.html │ │ └── helpers │ │ │ ├── ViewHelper.jl │ │ │ └── ValidationHelper.jl │ ├── config │ │ ├── env │ │ │ ├── global.jl │ │ │ ├── test.jl │ │ │ ├── dev.jl │ │ │ └── prod.jl │ │ └── initializers │ │ │ ├── autoload.jl │ │ │ ├── inflector.jl │ │ │ ├── converters.jl │ │ │ ├── searchlight.jl │ │ │ ├── ssl.jl │ │ │ └── logging.jl │ ├── routes.jl │ ├── .gitattributes │ └── .gitignore └── ssl │ ├── localhost.crt │ └── localhost.key ├── .gitignore ├── .gitattributes ├── .github ├── workflows │ ├── TagBot.yml │ ├── CompatHelper.yml │ └── ci.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── src ├── HTTPUtils.jl ├── renderers │ ├── html │ │ ├── form.jl │ │ └── select.jl │ └── MdHtml.jl ├── genie_types.jl ├── Flash.jl ├── constants.jl ├── Encryption.jl ├── Cache.jl ├── Responses.jl ├── Util.jl ├── Headers.jl ├── App.jl ├── Commands.jl ├── cache_adapters │ └── FileCache.jl ├── session_adapters │ └── FileSession.jl ├── FileTemplates.jl ├── Plugins.jl └── Exceptions.jl ├── CHANGELOG.html ├── LICENSE.md ├── CONTRIBUTING.md └── Project.toml /test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Genie_Caching.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Genie_Plugins.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/src/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/test/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Custom_Error_Pages.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Custom_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Genie_App_REPL.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Handling_Requests.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Handling_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Redirect_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Testing_Genie_Apps.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Using_Environments.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/db/seeds/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Configuration_Options.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Developing_MVC_Web_Apps.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Flax_Templating_Engine.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Important_Genie_Files.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Rendering_HTML_Layouts.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Rendering_JSON_Layouts.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/SearchLight_ORM_Support.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Sending_HTML_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Sending_JSON_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Sessions_And_Cookies.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Working_With_WebSockets.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/app/resources/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /files/new_app/db/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /docs/_todo_/Developing_Genie_Plugins.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Encrypting_Data_In_Genie.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Files_And_Folders_Structure.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Options_Preflight_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Plugins_AutoReload_Plugin.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Rendering_Markdown_Layouts.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Developing_Full_Stack_Genie_Apps.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Plugins_Authentication_Plugin.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Reloading_Code_In_Development.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Rendering_Exception_Responses.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Setting_Up_The_Asset_Pipeline.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Accelerated_Development_With_Generators.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Data_Validation_With_SearchLight_Validators.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Database_Versionsing_With_SearchLight_Migrations.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/src/tutorials/91--Deploying_Genie_Docker_Apps_on_Heroku.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Validating_Data_With_SearchLight_Model_Validators.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_todo_/Mastering_SearchLight_Models.md: -------------------------------------------------------------------------------- 1 | ... callbacks, validators ... -------------------------------------------------------------------------------- /test/views/argsencoded1.jl.html: -------------------------------------------------------------------------------- 1 |

Greetings

-------------------------------------------------------------------------------- /test/views/argsencoded3.jl.html: -------------------------------------------------------------------------------- 1 |

Greetings

-------------------------------------------------------------------------------- /test/views/argsencoded2.jl.html: -------------------------------------------------------------------------------- 1 |

Greetings

-------------------------------------------------------------------------------- /test/cors/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e" 3 | -------------------------------------------------------------------------------- /docs/_todo_/Mastering_Genie_Controllers.md: -------------------------------------------------------------------------------- 1 | ... before and after hooks and exceptional responses ... -------------------------------------------------------------------------------- /docs/content/img/genie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/docs/content/img/genie.gif -------------------------------------------------------------------------------- /docs/src/api/app.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = App 3 | ``` 4 | 5 | ```@docs 6 | bootstrap 7 | ``` 8 | -------------------------------------------------------------------------------- /files/new_app/config/env/global.jl: -------------------------------------------------------------------------------- 1 | # Place here configuration options that will be set for all environments 2 | -------------------------------------------------------------------------------- /files/new_app/routes.jl: -------------------------------------------------------------------------------- 1 | using Genie.Router 2 | 3 | route("/") do 4 | serve_static_file("welcome.html") 5 | end -------------------------------------------------------------------------------- /docs/content/img/genie_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/docs/content/img/genie_logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/build 2 | .tags 3 | .DS_Store 4 | .vscode 5 | test/build 6 | Manifest.toml 7 | test/cache 8 | test/sessions -------------------------------------------------------------------------------- /docs/src/api/httputils.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = HTTPUtils 3 | ``` 4 | 5 | ```@docs 6 | HTTPUtils.Dict 7 | ``` 8 | -------------------------------------------------------------------------------- /files/new_app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/favicon.ico -------------------------------------------------------------------------------- /docs/src/api/flash.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Flash 3 | ``` 4 | 5 | ```@docs 6 | flash 7 | flash_has_message 8 | ``` 9 | -------------------------------------------------------------------------------- /files/new_app/.gitattributes: -------------------------------------------------------------------------------- 1 | app/assets/* linguist-vendored 2 | public/* linguist-vendored 3 | *.jl.html linguist-language=HTML -------------------------------------------------------------------------------- /files/new_app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /files/new_app/public/img/genie/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/img/genie/docs.png -------------------------------------------------------------------------------- /files/new_app/public/img/genie/genie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/img/genie/genie.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | files/** linguist-vendored 2 | docs/** linguist-vendored 3 | test/** linguist-vendored 4 | *.jl.html linguist-language=HTML 5 | -------------------------------------------------------------------------------- /test/fileuploads/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e" 3 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 4 | -------------------------------------------------------------------------------- /test/views/view.jl.md: -------------------------------------------------------------------------------- 1 | # There are $(length(numbers)) 2 | 3 | $( 4 | for_each(numbers) do number 5 | " -> $number" 6 | end 7 | ) 8 | -------------------------------------------------------------------------------- /files/new_app/public/img/genie/community.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/img/genie/community.png -------------------------------------------------------------------------------- /files/new_app/public/img/genie/genie-sad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/img/genie/genie-sad.png -------------------------------------------------------------------------------- /docs/src/api/deploy-docker.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Deploy.Docker 3 | ``` 4 | 5 | ```@docs 6 | dockerfile 7 | build 8 | run 9 | ``` 10 | -------------------------------------------------------------------------------- /files/new_app/public/img/genie/contribute-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/img/genie/contribute-2.png -------------------------------------------------------------------------------- /docs/src/api/cache.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Cache 3 | ``` 4 | 5 | ```@docs 6 | withcache 7 | purge 8 | purgeall 9 | cachekey 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/src/api/encryption.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Encryption 3 | ``` 4 | 5 | ```@docs 6 | encrypt 7 | decrypt 8 | encryption_sauce 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/src/api/commands.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Commands 3 | ``` 4 | 5 | ```@docs 6 | execute 7 | parse_commandline_args 8 | called_command 9 | ``` 10 | -------------------------------------------------------------------------------- /docs/src/api/plugins.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Plugins 3 | ``` 4 | 5 | ```@docs 6 | recursive_copy 7 | congrats 8 | scaffold 9 | install 10 | ``` 11 | -------------------------------------------------------------------------------- /files/new_app/config/initializers/autoload.jl: -------------------------------------------------------------------------------- 1 | # Optional flat/non-resource MVC folder structure 2 | # push!(LOAD_PATH, "controllers", "views", "views/layouts", "models") -------------------------------------------------------------------------------- /docs/src/api/cookies.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Cookies 3 | ``` 4 | 5 | ```@docs 6 | get 7 | set! 8 | Cookies.Dict 9 | nullablevalue 10 | getcookies 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/src/api/headers.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Genie.Headers 3 | ``` 4 | 5 | ```@docs 6 | set_headers! 7 | normalize_headers 8 | normalize_header_key 9 | ``` 10 | -------------------------------------------------------------------------------- /files/new_app/public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /files/new_app/public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /files/new_app/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /files/new_app/public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/findmyway/Genie.jl/master/files/new_app/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /docs/src/api/deploy-heroku.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Deploy.Heroku 3 | ``` 4 | 5 | ```@docs 6 | createapp 7 | push 8 | release 9 | open 10 | login 11 | logs 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/src/api/renderer-js.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Renderer.Js 3 | ``` 4 | 5 | ```@docs 6 | get_template 7 | to_js 8 | render 9 | js 10 | Genie.Router.error 11 | ``` 12 | -------------------------------------------------------------------------------- /test/views/layout.jl.html: -------------------------------------------------------------------------------- 1 |
2 |

Layout header

3 |
4 | <% @yield %> 5 |
6 | 9 |
-------------------------------------------------------------------------------- /docs/Project.toml: -------------------------------------------------------------------------------- 1 | [deps] 2 | Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" 3 | Genie = "c43c736e-a2d1-11e8-161f-af95117fbd1e" 4 | 5 | [compat] 6 | Documenter = "0.27" 7 | Genie = "4" 8 | -------------------------------------------------------------------------------- /docs/src/api/filetemplates.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = FileTemplates 3 | ``` 4 | 5 | ```@docs 6 | newtask 7 | newcontroller 8 | newtest 9 | appmodule 10 | dockerfile 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/src/api/renderer-json.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Renderer.Json 3 | ``` 4 | 5 | ```@docs 6 | render 7 | Genie.Renderer.render 8 | json 9 | Genie.Router.error 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/src/api/util.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Util 3 | ``` 4 | 5 | ```@docs 6 | expand_nullable 7 | file_name_without_extension 8 | walk_dir 9 | time_to_unixtimestamp 10 | ``` 11 | -------------------------------------------------------------------------------- /test/views/view-vars.jl.md: -------------------------------------------------------------------------------- 1 | --- 2 | numbers: [1, 1, 2, 3, 5, 8, 13] 3 | --- 4 | 5 | # There are $(length(numbers)) 6 | 7 | $( 8 | for_each(numbers) do number 9 | " -> $number" 10 | end 11 | ) -------------------------------------------------------------------------------- /docs/src/api/configuration.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Configuration 3 | ``` 4 | 5 | ```@docs 6 | GENIE_VERSION 7 | isdev 8 | isprod 9 | istest 10 | env 11 | buildpath 12 | Settings 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/src/api/exceptions.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Exceptions 3 | ``` 4 | 5 | ```@docs 6 | ExceptionalResponse 7 | RuntimeException 8 | InternalServerException 9 | NotFoundException 10 | ``` 11 | -------------------------------------------------------------------------------- /files/new_app/config/initializers/inflector.jl: -------------------------------------------------------------------------------- 1 | import Inflector, Genie 2 | 3 | if ! isempty(Genie.config.inflector_irregulars) 4 | push!(Inflector.IRREGULAR_NOUNS, Genie.config.inflector_irregulars...) 5 | end -------------------------------------------------------------------------------- /files/new_app/public/js/genie/static.js: -------------------------------------------------------------------------------- 1 | $( document ).ready(function() { 2 | var ansi_up = new AnsiUp; 3 | $("code.language-julia").html(ansi_up.ansi_to_html( $("code.language-julia").text() )); 4 | }); 5 | -------------------------------------------------------------------------------- /test/views/layoutargsencoding.jl.html: -------------------------------------------------------------------------------- 1 |
2 |

Layout header

3 |
4 | <% @yield %> 5 |
6 | 9 |
-------------------------------------------------------------------------------- /docs/src/api/sessions.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Sessions 3 | ``` 4 | 5 | ```@docs 6 | Session 7 | id 8 | start 9 | set! 10 | get 11 | unset! 12 | isset 13 | persist 14 | load 15 | session 16 | init 17 | ``` 18 | -------------------------------------------------------------------------------- /test/views/outputscripttags.jl.html: -------------------------------------------------------------------------------- 1 |

Greetings

2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/src/api/assets.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Assets 3 | ``` 4 | 5 | ```@docs 6 | include_asset 7 | css_asset 8 | js_asset 9 | js_settings 10 | embedded 11 | channels 12 | channels_script 13 | channels_support 14 | favicon_support 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/src/api/responses.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Responses 3 | ``` 4 | 5 | ```@docs 6 | getresponse 7 | getheaders 8 | setheaders! 9 | setheaders 10 | getstatus 11 | setstatus! 12 | setstatus 13 | getbody 14 | setbody! 15 | setbody 16 | ``` 17 | -------------------------------------------------------------------------------- /files/new_app/config/initializers/converters.jl: -------------------------------------------------------------------------------- 1 | using Dates 2 | import Base.convert 3 | 4 | convert(::Type{Int}, v::SubString{String}) = parse(Int, v) 5 | convert(::Type{Float64}, v::SubString{String}) = parse(Float64, v) 6 | convert(::Type{Date}, s::String) = parse(Date, s) -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | schedule: 4 | - cron: 0 * * * * 5 | jobs: 6 | TagBot: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: JuliaRegistries/TagBot@v1 10 | with: 11 | token: ${{ secrets.GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /docs/src/api/appserver.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = AppServer 3 | ``` 4 | 5 | ```@docs 6 | ServersCollection 7 | SERVERS 8 | startup 9 | up 10 | down 11 | update_config 12 | handle_request 13 | setup_http_handler 14 | setup_ws_handler 15 | handle_ws_request 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/src/api/toolbox.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Toolbox 3 | ``` 4 | 5 | ```@docs 6 | TaskInfo 7 | TaskResult 8 | tasks 9 | VoidTaskResult 10 | validtaskname 11 | taskdocs 12 | loadtasks 13 | printtasks 14 | new 15 | taskfilename 16 | taskmodulename 17 | isvalidtask! 18 | ``` 19 | -------------------------------------------------------------------------------- /files/new_app/app/layouts/app.jl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Genie :: The Highly Productive Julia Web Framework 6 | 7 | 8 | <% 9 | @yield 10 | %> 11 | 12 | -------------------------------------------------------------------------------- /docs/src/api/inflector.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Inflector 3 | ``` 4 | 5 | ```@docs 6 | to_singular 7 | to_singular_irregular 8 | to_plural 9 | to_plural_irregular 10 | from_underscores 11 | is_singular 12 | is_plural 13 | irregulars 14 | irregular 15 | is_irregular 16 | IRREGULAR_NOUNS 17 | ``` 18 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | cd(@__DIR__) 2 | 3 | using Pkg 4 | 5 | using Test, TestSetExtensions, SafeTestsets, Logging 6 | using Genie 7 | 8 | Logging.global_logger(NullLogger()) 9 | 10 | @testset ExtendedTestSet "Genie tests" begin 11 | @includetests ARGS #[(endswith(t, ".jl") && t[1:end-3]) for t in ARGS] 12 | end -------------------------------------------------------------------------------- /docs/src/api/input.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Input 3 | ``` 4 | 5 | ```@docs 6 | HttpFile 7 | HttpInput 8 | HttpFormPart 9 | all 10 | post 11 | files 12 | post_from_request! 13 | post_url_encoded! 14 | post_multipart! 15 | get_multiform_parts! 16 | parse_seicolon_fields 17 | parse_quoted_params 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/src/api/requests.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Requests 3 | ``` 4 | 5 | ```@docs 6 | jsonpayload 7 | rawpayload 8 | filespayload 9 | infilespayload 10 | Requests.write 11 | Requests.read 12 | filename 13 | postpayload 14 | getpayload 15 | request 16 | payload 17 | matchedroute 18 | matchedchannel 19 | wsclient 20 | ``` 21 | -------------------------------------------------------------------------------- /files/new_app/app/helpers/ViewHelper.jl: -------------------------------------------------------------------------------- 1 | module ViewHelper 2 | 3 | using Genie, Genie.Flash, Genie.Router 4 | 5 | export output_flash 6 | 7 | function output_flash(flashtype::String = "danger") :: String 8 | flash_has_message() ? """
$(flash())
""" : "" 9 | end 10 | 11 | end -------------------------------------------------------------------------------- /files/new_app/config/initializers/searchlight.jl: -------------------------------------------------------------------------------- 1 | using SearchLight 2 | 3 | try 4 | SearchLight.Configuration.load() 5 | 6 | if SearchLight.config.db_config_settings["adapter"] !== nothing 7 | eval(Meta.parse("using SearchLight$(SearchLight.config.db_config_settings["adapter"])")) 8 | SearchLight.connect() 9 | end 10 | catch ex 11 | @error ex 12 | end -------------------------------------------------------------------------------- /test/cors/cors.jl: -------------------------------------------------------------------------------- 1 | using Genie, Genie.Router, Genie.Renderer.Json 2 | 3 | Genie.config.run_as_server = true 4 | Genie.config.cors_allowed_origins = ["*"] 5 | 6 | route("/random", method=POST) do 7 | dim = parse(Int, get(params(), :dim, "2")) 8 | num = parse(Int, get(params(), :num, "3")) 9 | 10 | (:random => rand(dim,num)) |> json 11 | end 12 | 13 | up(; open_browser = false) -------------------------------------------------------------------------------- /files/new_app/config/env/test.jl: -------------------------------------------------------------------------------- 1 | using Genie.Configuration, Logging 2 | 3 | const config = Settings( 4 | server_port = 8000, 5 | server_host = "127.0.0.1", 6 | log_level = Logging.Debug, 7 | log_to_file = true, 8 | server_handle_static_files = true 9 | ) 10 | 11 | ENV["JULIA_REVISE"] = "off" -------------------------------------------------------------------------------- /test/views/layoutscripttags.jl.html: -------------------------------------------------------------------------------- 1 | 2 |

Layout header

3 |
4 | <% @yield %> 5 |
6 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /files/new_app/db/connection.yml: -------------------------------------------------------------------------------- 1 | env: ENV["GENIE_ENV"] 2 | 3 | dev: 4 | adapter: 5 | database: 6 | host: 7 | username: 8 | password: 9 | port: 10 | config: 11 | 12 | prod: 13 | adapter: 14 | database: 15 | host: 16 | username: 17 | password: 18 | port: 19 | config: 20 | 21 | test: 22 | adapter: 23 | database: 24 | host: 25 | username: 26 | password: 27 | port: 28 | config: -------------------------------------------------------------------------------- /docs/src/api/renderer.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Renderer 3 | ``` 4 | 5 | ```@docs 6 | WebRenderable 7 | render 8 | redirect 9 | hasrequested 10 | respond 11 | registervars 12 | injectvars 13 | view_file_info 14 | vars_signature 15 | function_name 16 | m_name 17 | build_is_stale 18 | build_module 19 | preparebuilds 20 | purgebuilds 21 | changebuilds 22 | set_negotiated_content 23 | negotiate_content 24 | ``` 25 | -------------------------------------------------------------------------------- /test/tests_SVG_API.jl: -------------------------------------------------------------------------------- 1 | @safetestset "SVG API support in Renderer.Html" begin 2 | 3 | @safetestset "SVG API is available" begin 4 | using Genie 5 | using Genie.Renderer.Html 6 | import Genie.Util: fws 7 | 8 | @test svg() |> fws == "" |> fws 9 | 10 | @test_throws UndefVarError clippath() 11 | 12 | @test clipPath() |> fws == "" |> fws 13 | end; 14 | 15 | end; -------------------------------------------------------------------------------- /docs/src/api/genie.md: -------------------------------------------------------------------------------- 1 | ```@meta 2 | CurrentModule = Genie 3 | ``` 4 | 5 | ```@docs 6 | serve 7 | newapp 8 | newapp_webservice 9 | newapp_mvc 10 | newapp_fullstack 11 | loadapp 12 | startup 13 | up 14 | down 15 | run 16 | newcontroller 17 | newresource 18 | newtask 19 | load_libs 20 | load_resources 21 | load_helpers 22 | load_configurations 23 | load_initializers 24 | load_plugins 25 | load_routes_definitions 26 | secret_token 27 | default_context 28 | load 29 | replprint 30 | ``` 31 | -------------------------------------------------------------------------------- /test/tests_hello.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Hello Genie" begin 2 | 3 | using Genie, HTTP 4 | 5 | message = "Welcome to Genie!" 6 | 7 | route("/hello") do 8 | message 9 | end 10 | 11 | port = nothing 12 | port = rand(8500:8900) 13 | 14 | up(port) 15 | 16 | response = HTTP.get("http://localhost:$port/hello") 17 | 18 | @test response.status == 200 19 | @test String(response.body) == message 20 | 21 | down() 22 | sleep(1) 23 | server = nothing 24 | port = nothing 25 | end -------------------------------------------------------------------------------- /src/HTTPUtils.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Genie utilities for working over HTTP. 3 | """ 4 | module HTTPUtils 5 | 6 | import HTTP 7 | 8 | 9 | """ 10 | Base.Dict(req::HTTP.Request) :: Dict{String,String} 11 | 12 | Converts a `HTTP.Request` to a `Dict`. 13 | """ 14 | function Base.Dict(req::HTTP.Request) :: Dict{String,String} 15 | result = Dict{String,String}() 16 | for (k,v) in Dict(req.headers) 17 | result[lowercase(string(k))] = lowercase(string(v)) 18 | end 19 | 20 | result 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /test/formview.jl.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 11 |
12 | 13 |
-------------------------------------------------------------------------------- /test/tests_router.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Router tests" begin 2 | 3 | @safetestset "Basic routing" begin 4 | using Genie, Genie.Router 5 | 6 | route("/hello") do 7 | "Hello" 8 | end 9 | 10 | end; 11 | 12 | @safetestset "router_delete" begin 13 | using Genie, Genie.Router 14 | 15 | x = route("/caballo") do 16 | "caballo" 17 | end 18 | 19 | @test (x in routes()) == true 20 | Router.delete!(:get_caballo) 21 | @test (x in routes()) == false 22 | end; 23 | 24 | end; -------------------------------------------------------------------------------- /files/new_app/config/env/dev.jl: -------------------------------------------------------------------------------- 1 | using Genie.Configuration, Logging 2 | 3 | const config = Settings( 4 | server_port = 8000, 5 | server_host = "127.0.0.1", 6 | log_level = Logging.Info, 7 | log_to_file = false, 8 | server_handle_static_files = true, 9 | path_build = "build", 10 | format_julia_builds = true, 11 | format_html_output = true 12 | ) 13 | 14 | ENV["JULIA_REVISE"] = "auto" -------------------------------------------------------------------------------- /files/new_app/app/helpers/ValidationHelper.jl: -------------------------------------------------------------------------------- 1 | module ValidationHelper 2 | 3 | using Genie, SearchLight, SearchLight.Validation 4 | 5 | export output_errors 6 | 7 | function output_errors(m::T, field::Symbol)::String where {T<:SearchLight.AbstractModel} 8 | v = ispayload() ? validate(m) : ModelValidator() 9 | 10 | haserrorsfor(v, field) ? 11 | """ 12 |
13 | $(errors_to_string(v, field, separator = "
\n", uppercase_first = true)) 14 |
""" : "" 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /test/tests_assets_rendering.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Assets rendering" begin 2 | 3 | @safetestset "Embedded assets" begin 4 | using Genie 5 | using Genie.Renderer 6 | using Genie.Assets 7 | 8 | @test (Assets.js_settings() * Assets.embedded(Genie.Assets.asset_file(cwd=normpath(joinpath(@__DIR__, "..")), type="js", file="channels"))) == Assets.channels() 9 | 10 | @test Assets.channels()[1:18] == "window.Genie = {};" 11 | 12 | @test Assets.channels_script()[1:28] == "" 26 | @test Genie.Router.routes()[1].path == "/genie.jl/master/assets/js/$(hash(Genie.config.webchannels_default_route))/channels.js" 27 | @test Genie.Router.channels()[1].path == "/$(Genie.config.webchannels_default_route)/unsubscribe" 28 | @test Genie.Router.channels()[2].path == "/$(Genie.config.webchannels_default_route)/subscribe" 29 | 30 | @test favicon_support() == "" 31 | end 32 | 33 | end; 34 | -------------------------------------------------------------------------------- /test/tests_vars_rendering.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Vars rendering" begin 2 | using Genie 3 | using Genie.Renderer.Html, Genie.Requests 4 | 5 | greeting = "Welcome" 6 | name = "Genie" 7 | 8 | function htmlviewfile_withvars() 9 | raw" 10 |

$(vars(:greeting))

11 |
12 |

This is a $(vars(:name)) test

13 |
14 |
15 | " 16 | end 17 | 18 | function htmltemplatefile_withvars() 19 | raw" 20 | 21 | 22 | 23 | $(vars(:name)) test 24 | 25 | 26 |
27 | <% @yield %> 28 |
29 | 30 | 31 | 32 | " 33 | end 34 | 35 | @testset "String HTML rendering with vars" begin 36 | using Genie 37 | using Genie.Renderer.Html, Genie.Requests 38 | import Genie.Util: fws 39 | 40 | r = Requests.HTTP.Response() 41 | 42 | @testset "String no layout with vars" begin 43 | r = html(htmlviewfile_withvars(), greeting = greeting, name = name) 44 | 45 | @test String(r.body) |> fws == 46 | "

$greeting

This is a $name test

47 | " |> fws 48 | end; 49 | 50 | @testset "String with layout with vars" begin 51 | r = html(htmlviewfile_withvars(), layout = htmltemplatefile_withvars(), greeting = "Welcome", name = "Genie") 52 | 53 | @test String(r.body) |> fws == 54 | "$name test

$greeting

55 |

This is a $name test

56 | " |> fws 57 | end; 58 | 59 | @test r.status == 200 60 | @test r.headers[1]["Content-Type"] == "text/html; charset=utf-8" 61 | end; 62 | end; -------------------------------------------------------------------------------- /test/tests_output_script_tags.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Output """).body |> String == 7 | """

Good morning

""" 8 | 9 | @test Html.html("""

Good morning

""").body |> String == 10 | """

Good morning

""" 11 | 12 | @test Html.html("""

Good morning

""").body |> String == 13 | """

Good morning

""" 14 | 15 | @test Html.html("""

Good morning

""").body |> String == 16 | """

Good morning

""" 17 | 18 | @test Html.html("""

Good morning

""").body |> String == 19 | """

Good morning

""" 20 | 21 | @test Html.html(filepath("views/outputscripttags.jl.html")).body |> String |> fws == 22 | """

Greetings

23 | """ |> fws 24 | 25 | @test Html.html(filepath("views/outputscripttags.jl.html"), layout=filepath("views/layoutscripttags.jl.html")).body |> String |> fws == 26 | """

Layout header

Greetings

27 |
29 | """ |> fws 30 | end -------------------------------------------------------------------------------- /src/Util.jl: -------------------------------------------------------------------------------- 1 | module Util 2 | 3 | import Genie 4 | 5 | export expand_nullable, time_to_unixtimestamp 6 | 7 | 8 | 9 | """ 10 | expand_nullable{T}(value::Union{Nothing,T}, default::T) :: T 11 | 12 | Returns `value` if it is not `nothing` - otherwise `default`. 13 | """ 14 | function expand_nullable(value::Union{Nothing,T}, default::T)::T where T 15 | value === nothing ? default : value 16 | end 17 | 18 | 19 | """ 20 | file_name_without_extension(file_name, extension = ".jl") :: String 21 | 22 | Removes the file extension `extension` from `file_name`. 23 | """ 24 | function file_name_without_extension(file_name, extension = ".jl") :: String 25 | file_name[1:end-length(extension)] 26 | end 27 | 28 | 29 | """ 30 | function walk_dir(dir, paths = String[]; only_extensions = ["jl"], only_files = true, only_dirs = false) :: Vector{String} 31 | 32 | Recursively walks dir and `produce`s non directories. If `only_files`, directories will be skipped. If `only_dirs`, files will be skipped. 33 | """ 34 | function walk_dir(dir, paths = String[]; only_extensions = ["jl"], only_files = true, only_dirs = false) :: Vector{String} 35 | f = readdir(dir) 36 | 37 | for i in f 38 | full_path = joinpath(dir, i) 39 | 40 | if isdir(full_path) 41 | (! only_files || only_dirs) && push!(paths, full_path) 42 | walk_dir(full_path, paths; only_extensions = only_extensions) 43 | else 44 | only_dirs && continue 45 | 46 | ((last(split(i, ['.'])) in only_extensions) || isempty(only_extensions)) && push!(paths, full_path) 47 | end 48 | end 49 | 50 | paths 51 | end 52 | 53 | 54 | """ 55 | time_to_unixtimestamp(t::Float64 = time()) :: Int 56 | 57 | Converts a time value to the corresponding unix timestamp. 58 | """ 59 | function time_to_unixtimestamp(t::Float64 = time()) :: Int 60 | floor(t) |> Int 61 | end 62 | 63 | 64 | """ 65 | filterwhitespace(s::String, allowed::Vector{Char} = Char[]) :: String 66 | 67 | Removes whitespaces from `s`, whith the exception of the characters in `allowed`. 68 | """ 69 | function filterwhitespace(s::S, allowed::Vector{Char} = Char[])::String where {S<:AbstractString} 70 | filter(x -> (x in allowed) || ! isspace(x), string(s)) 71 | end 72 | 73 | const fws = filterwhitespace 74 | 75 | end 76 | -------------------------------------------------------------------------------- /docs/src/guides/Simple_API_backend.md: -------------------------------------------------------------------------------- 1 | # Developing a simple API backend 2 | 3 | Genie makes it very easy to quickly set up a REST API backend. All it takes is a few lines of code: 4 | 5 | ```julia 6 | using Genie 7 | import Genie.Router: route 8 | import Genie.Renderer.Json: json 9 | 10 | Genie.config.run_as_server = true 11 | 12 | route("/") do 13 | (:message => "Hi there!") |> json 14 | end 15 | 16 | Genie.startup() 17 | ``` 18 | 19 | The key bit here is `Genie.config.run_as_server = true`. This will start the server synchronously so the `startup()` function won't return. 20 | This endpoint can be run directly from the command line - if say, you save the code in a `rest.jl` file: 21 | 22 | ```shell 23 | $ julia rest.jl 24 | ``` 25 | 26 | ## Accepting JSON payloads 27 | 28 | One common requirement when exposing APIs is to accept `POST` payloads. That is, requests over `POST`, with a request body, usually as a JSON encoded object. We can build an echo service like this: 29 | 30 | ```julia 31 | using Genie, Genie.Router, Genie.Renderer.Json, Genie.Requests 32 | using HTTP 33 | 34 | route("/echo", method = POST) do 35 | message = jsonpayload() 36 | (:echo => (message["message"] * " ") ^ message["repeat"]) |> json 37 | end 38 | 39 | route("/send") do 40 | response = HTTP.request("POST", "http://localhost:8000/echo", [("Content-Type", "application/json")], """{"message":"hello", "repeat":3}""") 41 | 42 | response.body |> String |> json 43 | end 44 | 45 | Genie.startup(async = false) 46 | ``` 47 | 48 | Here we define two routes, `/send` and `/echo`. The `send` route makes a `HTTP` request over `POST` to `/echo`, sending a JSON payload with two values, `message` and `repeat`. 49 | In the `/echo` route, we grab the JSON payload using the `Requests.jsonpayload()` function, extract the values from the JSON object, and output the `message` value repeated for a number of times equal to the `repeat` value. 50 | 51 | If you run the code, the output should be 52 | 53 | ```javascript 54 | { 55 | echo: "hello hello hello " 56 | } 57 | ``` 58 | 59 | If the payload contains invalid JSON, the `jsonpayload` will be set to `nothing`. You can still access the raw payload by using the `Requests.rawpayload()` function. 60 | You can also use `rawpayload` if for example the type of request/payload is not JSON. 61 | -------------------------------------------------------------------------------- /src/Headers.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Provides functionality for working with HTTP headers in Genie. 3 | """ 4 | module Headers 5 | 6 | import HTTP 7 | import Genie 8 | 9 | """ 10 | set_headers!(req::HTTP.Request, res::HTTP.Response, app_response::HTTP.Response) :: HTTP.Response 11 | 12 | Configures the response headers. 13 | """ 14 | function set_headers!(req::HTTP.Request, res::HTTP.Response, app_response::HTTP.Response) :: HTTP.Response 15 | request_origin = get(Dict(req.headers), "Origin", "") 16 | 17 | if !isempty(request_origin) 18 | allowed_origin_dict = Dict("Access-Control-Allow-Origin" => 19 | in(request_origin, Genie.config.cors_allowed_origins) || in("*", Genie.config.cors_allowed_origins) 20 | ? request_origin 21 | : strip(Genie.config.cors_headers["Access-Control-Allow-Origin"]) 22 | ) 23 | 24 | app_response.headers = [d for d in 25 | merge( 26 | Genie.config.cors_headers, 27 | allowed_origin_dict, 28 | Dict(res.headers), 29 | Dict(app_response.headers) 30 | ) 31 | ] 32 | end 33 | 34 | app_response.headers = [d for d in 35 | merge( 36 | Dict(res.headers), 37 | Dict(app_response.headers), 38 | Dict("Server" => Genie.config.server_signature) 39 | ) 40 | ] 41 | 42 | app_response 43 | end 44 | 45 | 46 | """ 47 | normalize_headers(req::HTTP.Request) 48 | 49 | Makes request headers case insensitive. 50 | """ 51 | function normalize_headers(req::Union{HTTP.Request,HTTP.Response}) 52 | headers = Dict(req.headers) 53 | normalized_headers = Dict{String,String}() 54 | 55 | for (k,v) in headers 56 | normalized_headers[normalize_header_key(string(k))] = string(v) 57 | end 58 | 59 | req.headers = [k for k in normalized_headers] 60 | 61 | req 62 | end 63 | 64 | 65 | """ 66 | normalize_header_key(key::String) :: String 67 | 68 | Brings header keys to standard casing. 69 | """ 70 | function normalize_header_key(key::String) :: String 71 | join(map(x -> uppercasefirst(lowercase(x)), split(key, '-')), '-') 72 | end 73 | 74 | 75 | end -------------------------------------------------------------------------------- /docs/src/guides/Interactive_environment.md: -------------------------------------------------------------------------------- 1 | # Using Genie in an interactive environment (Jupyter/IJulia, REPL, etc) 2 | 3 | Genie can be used for ad-hoc exploratory programming, to quickly whip up a web server 4 | and expose your Julia functions. 5 | 6 | Once you have `Genie` into scope, you can define a new `route`. 7 | A `route` maps a URL to a function. 8 | 9 | ```julia 10 | julia> using Genie 11 | 12 | julia> route("/") do 13 | "Hi there!" 14 | end 15 | ``` 16 | 17 | You can now start the web server using 18 | 19 | ```julia 20 | julia> Genie.startup() 21 | ``` 22 | 23 | Finally, now navigate to – you should see the message "Hi there!". 24 | 25 | We can define more complex URIs which can also map to previously defined functions: 26 | 27 | ```julia 28 | julia> function hello_world() 29 | "Hello World!" 30 | end 31 | julia> route("/hello/world", hello_world) 32 | ``` 33 | 34 | Obviously, the functions can be defined anywhere (in any other module) as long as they are accessible in the current scope. 35 | 36 | You can now visit in the browser. 37 | 38 | Of course we can access GET params: 39 | 40 | ```julia 41 | julia> route("/echo/:message") do 42 | params(:message) 43 | end 44 | ``` 45 | 46 | Accessing should echo "ciao". 47 | 48 | And we can even match by types: 49 | 50 | ```julia 51 | julia> route("/sum/:x::Int/:y::Int") do 52 | params(:x) + params(:y) 53 | end 54 | ``` 55 | 56 | By default, GET params are extracted as `SubString` (more exactly, `SubString{String}`). 57 | If type constraints are added, Genie will attempt to convert the `SubString` to the indicated type. 58 | 59 | For the above to work, we also need to tell Genie how to perform the conversion: 60 | 61 | ```julia 62 | julia> import Base.convert 63 | julia> convert(::Type{Int}, s::AbstractString) = parse(Int, s) 64 | ``` 65 | 66 | Now if we access we should see `5` 67 | 68 | ## Handling query string params 69 | 70 | Query string params, which look like `...?foo=bar&baz=2` are automatically unpacked by Genie and placed into the `params` collection. For example: 71 | 72 | ```julia 73 | julia> route("/sum/:x::Int/:y::Int") do 74 | params(:x) + params(:y) + parse(Int, get(params, :initial_value, "0")) 75 | end 76 | ``` 77 | 78 | Accessing will now output `15`. 79 | -------------------------------------------------------------------------------- /src/App.jl: -------------------------------------------------------------------------------- 1 | """ 2 | App level functionality -- loading and managing app-wide components like configs, models, initializers, etc. 3 | """ 4 | module App 5 | 6 | import Genie 7 | 8 | 9 | ### PRIVATE ### 10 | 11 | 12 | """ 13 | bootstrap(context::Union{Module,Nothing} = nothing) :: Nothing 14 | 15 | Kickstarts the loading of a Genie app by loading the environment settings. 16 | """ 17 | function bootstrap(context::Union{Module,Nothing} = Genie.default_context(context)) :: Nothing 18 | if haskey(ENV, "GENIE_ENV") && isfile(joinpath(Genie.config.path_env, ENV["GENIE_ENV"] * ".jl")) 19 | isfile(joinpath(Genie.config.path_env, Genie.GLOBAL_ENV_FILE_NAME)) && Base.include(context, joinpath(Genie.config.path_env, Genie.GLOBAL_ENV_FILE_NAME)) 20 | isfile(joinpath(Genie.config.path_env, ENV["GENIE_ENV"] * ".jl")) && Base.include(context, joinpath(Genie.config.path_env, ENV["GENIE_ENV"] * ".jl")) 21 | else 22 | ENV["GENIE_ENV"] = Genie.Configuration.DEV 23 | isdefined(context, :config) || Core.eval(context, Meta.parse("const config = Genie.Configuration.Settings(app_env = Genie.Configuration.DEV)")) 24 | end 25 | 26 | haskey(ENV, "PORT") && (! isempty(ENV["PORT"])) && (context.config.server_port = parse(Int, ENV["PORT"])) 27 | haskey(ENV, "WSPORT") && (! isempty(ENV["WSPORT"])) && (context.config.websockets_port = parse(Int, ENV["WSPORT"])) 28 | haskey(ENV, "HOST") && (! isempty(ENV["HOST"])) && (context.config.server_host = ENV["HOST"]) 29 | haskey(ENV, "HOST") || (ENV["HOST"] = context.config.server_host) 30 | 31 | for f in fieldnames(typeof(context.config)) 32 | setfield!(Genie.config, f, getfield(context.config, f)) 33 | end 34 | 35 | printstyled(""" 36 | 37 | _____ _ 38 | | __|___ ___|_|___ 39 | | | | -_| | | -_| 40 | |_____|___|_|_|_|___| 41 | 42 | """, color = :red, bold = true) 43 | 44 | printstyled("| Web: https://genieframework.com\n", color = :light_black, bold = true) 45 | printstyled("| GitHub: https://github.com/genieframework/Genie.jl\n", color = :light_black, bold = true) 46 | printstyled("| Docs: https://www.genieframework.com/docs/tutorials/Overview.html\n", color = :light_black, bold = true) 47 | printstyled("| Discord: https://discord.com/invite/9zyZbD6J7H\n", color = :light_black, bold = true) 48 | printstyled("| Twitter: https://twitter.com/GenieMVC\n\n", color = :light_black, bold = true) 49 | printstyled("Active env: $(ENV["GENIE_ENV"] |> uppercase)\n\n", color = :light_blue, bold = true) 50 | 51 | nothing 52 | end 53 | 54 | end # module App -------------------------------------------------------------------------------- /test/tests_query_special_chars.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Special chars in GET params (query)" begin 2 | @safetestset " should be " begin 3 | using Genie 4 | using HTTP 5 | 6 | port = nothing 7 | port = rand(8500:8900) 8 | 9 | route("/") do 10 | params(:x) 11 | end 12 | 13 | server = up(port) 14 | 15 | response = try 16 | HTTP.request("GET", "http://127.0.0.1:$port/?x=foo+bar") 17 | catch ex 18 | ex.response 19 | end 20 | 21 | @test response.status == 200 22 | @test String(response.body) == "foo bar" 23 | 24 | down() 25 | sleep(1) 26 | server = nothing 27 | port = nothing 28 | end; 29 | 30 | @safetestset " should be " begin 31 | using Genie 32 | using HTTP 33 | 34 | port = nothing 35 | port = rand(8500:8900) 36 | 37 | route("/") do 38 | params(:x) 39 | end 40 | 41 | server = up(port) 42 | 43 | response = try 44 | HTTP.request("GET", "http://127.0.0.1:$port/?x=foo%20bar") 45 | catch ex 46 | ex.response 47 | end 48 | 49 | @test response.status == 200 50 | @test String(response.body) == "foo bar" 51 | 52 | down() 53 | sleep(1) 54 | server = nothing 55 | port = nothing 56 | end; 57 | 58 | @safetestset " should be " begin 59 | using Genie 60 | using HTTP 61 | 62 | port = nothing 63 | port = rand(8500:8900) 64 | 65 | route("/") do 66 | params(:x) 67 | end 68 | 69 | server = up(port) 70 | 71 | response = try 72 | HTTP.request("GET", "http://127.0.0.1:$port/?x=foo%2Bbar") 73 | catch ex 74 | ex.response 75 | end 76 | 77 | @test response.status == 200 78 | @test String(response.body) == "foo+bar" 79 | 80 | down() 81 | sleep(1) 82 | server = nothing 83 | port = nothing 84 | end; 85 | 86 | @safetestset "emoji support" begin 87 | using Genie 88 | using HTTP 89 | 90 | port = nothing 91 | port = rand(8500:8900) 92 | 93 | route("/") do 94 | params(:x) 95 | end 96 | 97 | server = up(port) 98 | 99 | response = try 100 | HTTP.request("GET", "http://127.0.0.1:$port/?x=✔+🧞+♥") 101 | catch ex 102 | ex.response 103 | end 104 | 105 | @test response.status == 200 106 | @test String(response.body) == "✔ 🧞 ♥" 107 | 108 | down() 109 | sleep(1) 110 | server = nothing 111 | port = nothing 112 | end; 113 | 114 | end -------------------------------------------------------------------------------- /files/new_app/public/css/genie/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.17.1 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=julia */ 3 | /** 4 | * okaidia theme for JavaScript, CSS and HTML 5 | * Loosely based on Monokai textmate theme by http://www.monokai.nl/ 6 | * @author ocodia 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: #f8f8f2; 12 | background: none; 13 | text-shadow: 0 1px rgba(0, 0, 0, 0.3); 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | /* Code blocks */ 34 | pre[class*="language-"] { 35 | padding: 1em; 36 | margin: .5em 0; 37 | overflow: auto; 38 | border-radius: 0.3em; 39 | } 40 | 41 | :not(pre) > code[class*="language-"], 42 | pre[class*="language-"] { 43 | background: #272822; 44 | } 45 | 46 | /* Inline code */ 47 | :not(pre) > code[class*="language-"] { 48 | padding: .1em; 49 | border-radius: .3em; 50 | white-space: normal; 51 | } 52 | 53 | .token.comment, 54 | .token.prolog, 55 | .token.doctype, 56 | .token.cdata { 57 | color: slategray; 58 | } 59 | 60 | .token.punctuation { 61 | color: #f8f8f2; 62 | } 63 | 64 | .namespace { 65 | opacity: .7; 66 | } 67 | 68 | .token.property, 69 | .token.tag, 70 | .token.constant, 71 | .token.symbol, 72 | .token.deleted { 73 | color: #f92672; 74 | } 75 | 76 | .token.boolean, 77 | .token.number { 78 | color: #ae81ff; 79 | } 80 | 81 | .token.selector, 82 | .token.attr-name, 83 | .token.string, 84 | .token.char, 85 | .token.builtin, 86 | .token.inserted { 87 | color: #a6e22e; 88 | } 89 | 90 | .token.operator, 91 | .token.entity, 92 | .token.url, 93 | .language-css .token.string, 94 | .style .token.string, 95 | .token.variable { 96 | color: #f8f8f2; 97 | } 98 | 99 | .token.atrule, 100 | .token.attr-value, 101 | .token.function, 102 | .token.class-name { 103 | color: #e6db74; 104 | } 105 | 106 | .token.keyword { 107 | color: #66d9ef; 108 | } 109 | 110 | .token.regex, 111 | .token.important { 112 | color: #fd971f; 113 | } 114 | 115 | .token.important, 116 | .token.bold { 117 | font-weight: bold; 118 | } 119 | .token.italic { 120 | font-style: italic; 121 | } 122 | 123 | .token.entity { 124 | cursor: help; 125 | } 126 | 127 | -------------------------------------------------------------------------------- /test/tests_markdown_rendering.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Markdown rendering" begin 2 | 3 | @safetestset "String markdown rendering" begin 4 | using Genie 5 | using Genie.Renderer.Html 6 | using Markdown 7 | import Genie.Util: fws 8 | 9 | view = raw""" 10 | # Hello 11 | ## Welcome to Genie""" |> Markdown.parse 12 | 13 | @test (Html.html(view, forceparse = true).body |> String |> fws) == 14 | "

Hello

Welcome to Genie

" |> fws 15 | 16 | view = raw""" 17 | # Hello 18 | ## Welcome to Genie, $name""" |> Markdown.parse 19 | 20 | @test (Html.html(view, name = "John").body |> String |> fws) == 21 | "

Hello

Welcome to Genie, John

" |> fws 22 | 23 | layout = raw""" 24 |
25 |

Layout header

26 |
27 | <% @yield %> 28 |
29 |
30 |

Layout footer

31 |
32 |
""" 33 | 34 | @test (Html.html(view, layout = layout, name = "John").body |> String |> fws) == 35 | "

Layout header

Hello

Welcome to Genie, John

36 |

Layout footer

" |> fws 37 | end; 38 | 39 | @safetestset "Template markdown rendering" begin 40 | using Genie, Genie.Renderer 41 | using Genie.Renderer.Html 42 | import Genie.Util: fws 43 | 44 | @test Html.html(filepath("views/view.jl.md"), numbers = [1, 1, 2, 3, 5, 8, 13]).body |> String |> fws == 45 | """ 46 |

There are 7

47 |

-> 1 -> 1 -> 2 -> 3 -> 5 -> 8 -> 13

48 | """ |> fws 49 | 50 | @test Html.html(filepath("views/view.jl.md"), layout = filepath("views/layout.jl.html"), numbers = [1, 1, 2, 3, 5, 8, 13]).body |> String |> fws == 51 | """ 52 |

Layout header

There are 7

53 |

-> 1 -> 1 -> 2 -> 3 -> 5 -> 8 -> 13

54 |

Layout footer

""" |> fws 55 | end 56 | 57 | @safetestset "Markdown rendering with embedded variables" begin 58 | using Genie, Genie.Renderer 59 | using Genie.Renderer.Html 60 | import Genie.Util: fws 61 | 62 | @test Html.html(filepath("views/view-vars.jl.md")).body |> String |> fws == 63 | """ 64 |

There are 7

65 |

-> 1 -> 1 -> 2 -> 3 -> 5 -> 8 -> 13

66 | """ |> fws 67 | end; 68 | 69 | end; -------------------------------------------------------------------------------- /docs/src/tutorials/13--Initializers.md: -------------------------------------------------------------------------------- 1 | # Customized application configuration with initializers 2 | 3 | Initializers are plain Julia files which are loaded early in the application cycle (before routes, controller, or models). They are designed to expose configuration code which might be needed later on in the application life cycle. 4 | 5 | Initializers should be placed within the `config/initializers/` folder and they will be automatically loaded (included) by Genie. 6 | 7 | **If your configuration is environment dependent (ie a database connection which is different between dev and prod), it should be added to the corresponding `config/env/*.jl` file.** 8 | 9 | ## Best practices 10 | 11 | * You can name the initializers as you wish (ideally a descriptive name, like maybe `redis.jl`). 12 | * Don't use uppercase names unless you define a module (in order to respect Julia's naming practices). 13 | * Keep your initializer files small and focused, so they serve only one purpose. 14 | * You can add as many initializers as you need. 15 | * Do not abuse them, they are not meant to host complex code. 16 | 17 | ## Load order 18 | 19 | The initializers are loaded in the order they are read from the file system. If you have initializers which depend on other initializers, this is most likely a sign that you need to refactor using a model or a library file. 20 | 21 | --- 22 | **HEADS UP** 23 | 24 | Library files are Julia files which provide distinct functionality and can be placed in the `lib/` folder where they are also automatically loaded by Genie. If the `lib/` folder does not exist, you can create it yourself. 25 | 26 | --- 27 | 28 | ## Scope 29 | 30 | All the definitions (variables, constants, functions, modules, etc) added to initializer files are loaded into your app's module. So if your app is called `MyGenieApp`, the definitions will be available under the `MyGenieApp` module. 31 | 32 | --- 33 | **HEADS UP** 34 | 35 | Given that your app's name is variable, you can also access your app's module through the `UserApp` constant. So all the definitions added to initializers can also be accessed through the `UserApp` module (`UserApp === MyGenieApp`). 36 | 37 | --- 38 | 39 | ## Example 40 | 41 | This is Genie's default initializer for loading the SearchLight ORM. Please notice that the initializers does contain database configuration information, which is defined in a dedicated, environment dependent file. The initializer simply delegates the configuration loading and setup to the SearchLight package. 42 | 43 | ```julia 44 | using SearchLight, SearchLight.QueryBuilder 45 | 46 | SearchLight.Configuration.load() 47 | 48 | if SearchLight.config.db_config_settings["adapter"] !== nothing 49 | SearchLight.Database.setup_adapter() 50 | SearchLight.Database.connect() 51 | SearchLight.load_resources() 52 | end 53 | ``` 54 | -------------------------------------------------------------------------------- /test/tests_arguments_encoding.jl: -------------------------------------------------------------------------------- 1 | @safetestset "Escaping quotes" begin 2 | @safetestset "Double quoted arguments" begin 3 | using Genie, Genie.Renderer 4 | using Genie.Renderer.Html 5 | 6 | @test Html.html("""

Good morning

""").body |> String == 7 | """

Good morning

""" 8 | 9 | @test Html.html("""

Good morning

""").body |> String == 10 | """

Good morning

""" 11 | 12 | @test Html.html("""

Good morning

""").body |> String == 13 | """

Good morning

""" 14 | 15 | @test Html.html("""

Good morning

""").body |> String == 16 | """

Good morning

""" 17 | end 18 | 19 | @safetestset "Single quoted arguments" begin 20 | using Genie, Genie.Renderer 21 | using Genie.Renderer.Html 22 | 23 | @test Html.html("""

Good morning

""").body |> String == 24 | """

Good morning

""" 25 | 26 | @test Html.html("""

Good morning

""").body |> String == 27 | """

Good morning

""" 28 | 29 | @test Html.html("""

Good morning

""").body |> String == 30 | """

Good morning

""" 31 | end 32 | 33 | @safetestset "Arguments in templates" begin 34 | using Genie, Genie.Renderer 35 | using Genie.Renderer.Html 36 | import Genie.Util: fws 37 | 38 | @test Html.html(filepath("views/argsencoded1.jl.html")).body |> String |> fws == 39 | """

Greetings

""" |> fws 40 | 41 | @test_throws LoadError Html.html(filepath("views/argsencoded2.jl.html")).body |> String == 42 | """

Greetings

""" 43 | 44 | @test Html.html(filepath("views/argsencoded3.jl.html")).body |> String |> fws == 45 | """

Greetings

""" |> fws 46 | 47 | 48 | @test Html.html(filepath("views/argsencoded1.jl.html"), layout=filepath("views/layoutargsencoding.jl.html")).body |> String |> fws == 49 | """

Layout header

50 |

Greetings

Layout footer

51 | """ |> fws 52 | end 53 | end -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "Genie" 2 | uuid = "c43c736e-a2d1-11e8-161f-af95117fbd1e" 3 | authors = ["Adrian Salceanu "] 4 | version = "4.11.0" 5 | 6 | [deps] 7 | ArgParse = "c7e460c6-2fb9-53a9-8c5b-16f535851c63" 8 | Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 9 | Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" 10 | EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615" 11 | FilePathsBase = "48062228-2e41-5def-b9a4-89aafe57970f" 12 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 13 | HttpCommon = "77172c1b-203f-54ac-aa54-3f1198fe9f90" 14 | Inflector = "6d011eab-0732-4556-8808-e463c76bf3b6" 15 | JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" 16 | JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" 17 | Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" 18 | Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" 19 | MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d" 20 | Millboard = "39ec1447-df44-5f4c-beaa-866f30b4d3b2" 21 | Nettle = "49dea1ee-f6fa-5aa6-9a11-8816cee7d4b9" 22 | OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" 23 | Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" 24 | REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" 25 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 26 | Reexport = "189a3867-3050-52da-a836-e630ba90ab69" 27 | Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" 28 | SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" 29 | Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" 30 | Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" 31 | UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" 32 | Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" 33 | VersionCheck = "a637dc6b-bca1-447e-a4fa-35264c9d0580" 34 | YAML = "ddb6d928-2868-570f-bddf-ab3f9cf99eb6" 35 | 36 | [compat] 37 | ArgParse = "1" 38 | EzXML = "1" 39 | FilePathsBase = "0.9" 40 | HTTP = "0.8, 0.9" 41 | HttpCommon = "0.5" 42 | Inflector = "1" 43 | JSON3 = "1" 44 | JuliaFormatter = "0.22" 45 | MbedTLS = "1" 46 | Millboard = "0.2" 47 | Nettle = "0.5" 48 | OrderedCollections = "1" 49 | Reexport = "0.2, 1" 50 | Revise = "2, 3" 51 | VersionCheck = "0.1, 0.2, 1" 52 | YAML = "0.4" 53 | julia = "1.6" 54 | 55 | [extras] 56 | Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 57 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 58 | Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" 59 | LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" 60 | Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" 61 | Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" 62 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 63 | Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" 64 | SafeTestsets = "1bc83da4-3b8d-516f-aca4-4fe02f6d838f" 65 | Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" 66 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 67 | TestSetExtensions = "98d24dd4-01ad-11ea-1b02-c9a08f80db04" 68 | 69 | [targets] 70 | test = ["SafeTestsets", "Test", "TestSetExtensions", "Pkg", "Random", "Revise", "Logging", "LoggingExtras", "Sockets", "Dates", "HTTP", "Markdown"] 71 | -------------------------------------------------------------------------------- /test/tests_z_HEAD_requests.jl: -------------------------------------------------------------------------------- 1 | @safetestset "HEAD requests" begin 2 | @safetestset "HEAD requests should be by default handled by GET" begin 3 | using Genie 4 | using HTTP 5 | 6 | port = nothing 7 | port = rand(8500:8900) 8 | 9 | route("/") do 10 | "GET request" 11 | end 12 | 13 | server = up(port) 14 | 15 | response = try 16 | HTTP.request("GET", "http://127.0.0.1:$port", ["Content-Type" => "text/html"]) 17 | catch ex 18 | ex.response 19 | end 20 | 21 | @test response.status == 200 22 | @test String(response.body) == "GET request" 23 | 24 | response = try 25 | HTTP.request("HEAD", "http://127.0.0.1:$port", ["Content-Type" => "text/html"]) 26 | catch ex 27 | ex.response 28 | end 29 | 30 | @test response.status == 200 31 | @test String(response.body) == "" 32 | 33 | down() 34 | sleep(1) 35 | server = nothing 36 | port = nothing 37 | end; 38 | 39 | @safetestset "HEAD requests have no body" begin 40 | using Genie 41 | using HTTP 42 | 43 | port = nothing 44 | port = rand(8500:8900) 45 | 46 | route("/") do 47 | "Hello world" 48 | end 49 | 50 | route("/", method = HEAD) do 51 | "Hello world" 52 | end 53 | 54 | server = up(port; open_browser = false) 55 | 56 | response = try 57 | HTTP.request("GET", "http://127.0.0.1:$port", ["Content-Type" => "text/html"]) 58 | catch ex 59 | ex.response 60 | end 61 | 62 | @test response.status == 200 63 | @test String(response.body) == "Hello world" 64 | 65 | response = try 66 | HTTP.request("HEAD", "http://127.0.0.1:$port", ["Content-Type" => "text/html"]) 67 | catch ex 68 | ex.response 69 | end 70 | @test response.status == 200 71 | @test isempty(String(response.body)) == true 72 | 73 | down() 74 | sleep(1) 75 | server = nothing 76 | port = nothing 77 | end; 78 | 79 | @safetestset "HEAD requests should overwrite GET" begin 80 | using Genie 81 | using HTTP 82 | 83 | port = nothing 84 | port = rand(8500:8900) 85 | 86 | request_method = "" 87 | 88 | route("/", named = :get_root) do 89 | request_method = "GET" 90 | "GET request" 91 | end 92 | 93 | route("/", method = "HEAD", named = :head_root) do 94 | request_method = "HEAD" 95 | "HEAD request" 96 | end 97 | 98 | server = up(port) 99 | sleep(1) 100 | 101 | response = try 102 | HTTP.request("GET", "http://127.0.0.1:$port", ["Content-Type" => "text/html"]) 103 | catch ex 104 | ex.response 105 | end 106 | 107 | @test response.status == 200 108 | @test request_method == "GET" 109 | 110 | response = try 111 | HTTP.request("HEAD", "http://127.0.0.1:$port", ["Content-Type" => "text/html"]) 112 | catch ex 113 | ex.response 114 | end 115 | 116 | @test response.status == 200 117 | @test request_method == "HEAD" 118 | 119 | down() 120 | sleep(1) 121 | server = nothing 122 | port = nothing 123 | end; 124 | 125 | end; 126 | -------------------------------------------------------------------------------- /src/Commands.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Handles command line arguments for the genie.jl script. 3 | """ 4 | module Commands 5 | 6 | import Sockets 7 | import ArgParse 8 | import Genie 9 | using Logging 10 | 11 | """ 12 | execute(config::Settings) :: Nothing 13 | 14 | Runs the requested Genie app command, based on the `args` passed to the script. 15 | """ 16 | function execute(config::Genie.Configuration.Settings; server::Union{Sockets.TCPServer,Nothing} = nothing) :: Nothing 17 | parsed_args = parse_commandline_args(config)::Dict{String,Any} 18 | 19 | # overwrite env settings with command line arguments 20 | Genie.config.app_env = ENV["GENIE_ENV"] 21 | Genie.config.server_port = haskey(ENV, "PORT") ? parse(Int, ENV["PORT"]) : parse(Int, parsed_args["p"]) 22 | Genie.config.websockets_port = haskey(ENV, "WSPORT") ? parse(Int, ENV["WSPORT"]) : parse(Int, parsed_args["w"]) 23 | Genie.config.server_host = parsed_args["l"] 24 | 25 | if called_command(parsed_args, "s") || (haskey(ENV, "STARTSERVER") && parse(Bool, ENV["STARTSERVER"])) 26 | Genie.config.run_as_server = true 27 | Base.invokelatest(Genie.up, Genie.config.server_port, Genie.config.server_host; server = server) 28 | 29 | elseif called_command(parsed_args, "r") 30 | endswith(parsed_args["r"], "Task") || (parsed_args["r"] *= "Task") 31 | Base.invokelatest(Genie.Toolbox.loadtasks, Main.UserApp) 32 | taskname = parsed_args["r"] 33 | task = getfield(Main.UserApp, Symbol(taskname)) 34 | 35 | if parsed_args["a"] !== nothing 36 | Base.invokelatest(task.runtask, parsed_args["a"]) 37 | else 38 | Base.invokelatest(task.runtask) 39 | end 40 | end 41 | 42 | nothing 43 | end 44 | 45 | 46 | """ 47 | parse_commandline_args() :: Dict{String,Any} 48 | 49 | Extracts the command line args passed into the app and returns them as a `Dict`, possibly setting up defaults. 50 | Also, it is used by the ArgParse module to populate the command line help for the app `-h`. 51 | """ 52 | function parse_commandline_args(config::Genie.Configuration.Settings) :: Dict{String,Any} 53 | settings = ArgParse.ArgParseSettings() 54 | 55 | settings.description = "Genie web framework CLI" 56 | settings.epilog = "Visit https://genieframework.com for more info" 57 | 58 | ArgParse.@add_arg_table! settings begin 59 | "s" 60 | help = "starts HTTP server" 61 | 62 | "-p" 63 | help = "HTTP server port" 64 | default = "$(config.server_port)" 65 | 66 | "-w" 67 | help = "Web Sockets server port" 68 | default = "$(config.server_port)" 69 | 70 | "-l" 71 | help = "Host IP to listen on" 72 | default = "$(config.server_host)" 73 | 74 | "-r" 75 | help = "runs Genie.Toolbox task" 76 | 77 | "-a" 78 | help = "additional arguments passed into the Genie.Toolbox `runtask` function" 79 | default = nothing 80 | end 81 | 82 | ArgParse.parse_args(settings) 83 | end 84 | 85 | 86 | """ 87 | called_command(args::Dict, key::String) :: Bool 88 | 89 | Checks whether or not a certain command was invoked by looking at the command line args. 90 | """ 91 | function called_command(args::Dict{String,Any}, key::String) :: Bool 92 | haskey(args, key) && (args[key] == "true" || args["s"] == key || args[key] !== nothing) 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /docs/src/guides/Frontend_assets.md: -------------------------------------------------------------------------------- 1 | # Frontend assets 2 | 3 | Genie makes use of Yarn and Webpack to compile and serve frontend assets. In fact out of the box a config file making use of Webpack4's most popular features is supplied, as well as Bootstrap4 and jQuey pre-installed. That way you can focus on your web app, taking yet another layer of abstraction away. 4 | 5 | As summary: 6 | 7 | - production minimizes files, separating CSS from JS 8 | - development uses webpack-dev-server on node port 3000 (relies on inbuilt socket server) 9 | - supported file formats: css, scss, sass, js, coffee 10 | - output is saved to `public/dist` of your Genie app 11 | - pre-configured with Bootstrap4 and jQuery 12 | - supports bundling (chunks and async loading) 13 | 14 | ## Requirements 15 | 16 | ** In order for the Genie app to install the asset pipeline the app needs to be created using the `fullstack = true` option, as in: ** 17 | 18 | ```julia 19 | julia> Genie.newapp("MyApp", fullstack = true) 20 | ``` 21 | 22 | 23 | You will need NodeJS as well as Yarn to be installed on system. If using Linux, macOS your package manager should easily allow this. Windows please download relevant installer from NodeJS project webpage. 24 | 25 | ## Installing dependencies 26 | 27 | From app directory: 28 | 29 | ``` 30 | yarn install 31 | ``` 32 | 33 | ## Development mode 34 | 35 | From app directory: 36 | 37 | ``` 38 | yarn run develop 39 | ``` 40 | 41 | Static files will be served via Node server websocket on port 3000. Sourcemaps will be supplied as well, allowing you to easily debug. 42 | 43 | Any changes you make to static files will automatically be sent to browser. If working with React/Vue, state of page will be conserved during process. Save time without re-compiling and reloading page. 44 | 45 | Genie by default runs in development mode. Hence if using `app/assets/js/application.js` as main entry point, no additional configurations are required. 46 | 47 | ## Production mode 48 | 49 | From app directory: 50 | 51 | ``` 52 | yarn run build 53 | ``` 54 | 55 | This will output minified files to `public/dist` dir of your app, without source maps. 56 | 57 | Please run Genie in production mode to serve static assets. 58 | 59 | ## Considerations 60 | 61 | In order to take best advantage of Webpack bundling, it is recommended to serve all static files (images, fonts) via JS `require` calls. Let Webpack optimise bundle. 62 | 63 | Lazy-loading is also supported. This means that browser will fetch ressources when required, speeding-up page loads. As an example, consider chat integration via a button. With demo code below, the chat functionality will only be fetched by browser once user clicks button. 64 | 65 | ``` 66 | button.onclick = () => { 67 | import("./chat").then(chat => { 68 | chat.init() 69 | }) 70 | } 71 | ``` 72 | 73 | ## Minimal Bootstrap integration 74 | 75 | Following Webpack philisophy, it is recommended to only load library dependencies when necessary. Nonetheless for Bootstrap to work, one can do as follows: 76 | 77 | - under `app/assets/js/application.js`, add `import "bootstrap";` 78 | - create new file `app/assets/css/vendor.scss` and add `@import "~bootstrap/scss/bootstrap.scss";` 79 | - include this new file from `app/assets/js/application.js`, by adding `require("../css/vendor.scss");` 80 | -------------------------------------------------------------------------------- /src/cache_adapters/FileCache.jl: -------------------------------------------------------------------------------- 1 | module FileCache 2 | 3 | import Serialization 4 | import Genie, Genie.Cache 5 | 6 | #===# 7 | # IMPLEMENTATION # 8 | 9 | """ 10 | tocache(key::Any, content::Any; dir::String = "") :: Nothing 11 | 12 | Persists `content` onto the file system under the `key` key. 13 | """ 14 | function tocache(key::Any, content::Any; dir::String = "") :: Nothing 15 | open(cache_path(string(key), dir = dir), "w") do io 16 | Serialization.serialize(io, content) 17 | end 18 | 19 | nothing 20 | end 21 | 22 | 23 | """ 24 | fromcache(key::Any, expiration::Int; dir::String = "") :: Union{Nothing,Any} 25 | 26 | Retrieves from cache the object stored under the `key` key if the `expiration` delta (in seconds) is in the future. 27 | """ 28 | function fromcache(key::Any, expiration::Int; dir::String = "") :: Union{Nothing,Any} 29 | file_path = cache_path(string(key), dir = dir) 30 | 31 | ( ! isfile(file_path) || stat(file_path).ctime + expiration < time() ) && return nothing 32 | 33 | try 34 | open(file_path) do io 35 | Serialization.deserialize(io) 36 | end 37 | catch ex 38 | @warn ex 39 | nothing 40 | end 41 | end 42 | 43 | 44 | """ 45 | cache_path(key::Any; dir::String = "") :: String 46 | 47 | Computes the path to a cache `key` based on current cache settings. 48 | """ 49 | function cache_path(key::Any; dir::String = "") :: String 50 | path = joinpath(Genie.config.path_cache, dir) 51 | ! isdir(path) && mkpath(path) 52 | 53 | joinpath(path, string(key)) 54 | end 55 | 56 | 57 | #===# 58 | # INTERFACE # 59 | 60 | 61 | """ 62 | withcache(f::Function, key::Any, expiration::Int = Genie.config.cache_duration; dir = "", condition::Bool = true) 63 | 64 | Executes the function `f` and stores the result into the cache for the duration (in seconds) of `expiration`. Next time the function is invoked, 65 | if the cache has not expired, the cached result is returned skipping the function execution. 66 | The optional `dir` param is used to designate the folder where the cache will be stored (within the configured cache folder). 67 | If `condition` is `false` caching will be skipped. 68 | """ 69 | function Genie.Cache.withcache(f::Function, key::Any, expiration::Int = Genie.config.cache_duration; dir::String = "", condition::Bool = true) 70 | ( expiration == 0 || ! condition ) && return f() 71 | 72 | cached_data = fromcache(Genie.Cache.cachekey(string(key)), expiration, dir = dir) 73 | 74 | if cached_data === nothing 75 | output = f() 76 | tocache(Genie.Cache.cachekey(string(key)), output, dir = dir) 77 | 78 | return output 79 | end 80 | 81 | cached_data 82 | end 83 | 84 | 85 | """ 86 | purge(key::Any) :: Nothing 87 | 88 | Removes the cache data stored under the `key` key. 89 | """ 90 | function Genie.Cache.purge(key::Any; dir::String = "") :: Nothing 91 | rm(cache_path(Genie.Cache.cachekey(string(key)), dir = dir)) 92 | 93 | nothing 94 | end 95 | 96 | 97 | """ 98 | purgeall(; dir::String = "") :: Nothing 99 | 100 | Removes all cached data. 101 | """ 102 | function Genie.Cache.purgeall(; dir::String = "") :: Nothing 103 | rm(cache_path("", dir = dir), recursive = true) 104 | mkpath(cache_path("", dir = dir)) 105 | 106 | nothing 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /files/new_app/public/welcome.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Genie :: The highly productive Julia web framework 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |

Welcome!

22 |

It works! You have successfully created and started your Genie app.

23 |
24 | 27 |
28 | 29 |
30 | 31 | 32 |
33 |
34 |

What next?

35 |
41 |
42 | The docs 43 |

Contribute

44 |

Add your favorite new features, report issues, or squash some bugs.

45 | Visit GitHub page 46 |
47 |
48 | Get in touch 49 |

Get in touch

50 |

Come say "Hi!" -- join Genie's community on Gitter.

51 | Go to Gitter 52 |
53 |
54 |
55 | 56 | 57 | 58 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /files/new_app/public/error-xxx.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Genie :: The highly productive Julia web framework 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |

22 | 23 |

24 |

25 | 26 | 27 | 28 |

29 |
30 | Go Home 31 |
32 | 35 |
36 | 37 |
38 | 39 | 40 |
41 |
42 |

What next?

43 |
44 | The docs 45 |

The docs

46 |

Check out the guides, try the walk-throughs, or read the API docs.

47 | Read the docs 48 |
49 |
50 | The docs 51 |

Contribute

52 |

Add your favorite new features, report issues, or squash some bugs.

53 | Visit GitHub page 54 |
55 |
56 | Get in touch 57 |

Get in touch

58 |

Come say "Hi!" -- join Genie's community on Gitter.

59 | Go to Gitter 60 |
61 |
62 |
63 | 64 | 65 | 66 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /files/new_app/public/error-404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Genie :: The highly productive Julia web framework 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 |
21 |

404 Not Found

22 |

Sorry, we can not find 23 | 24 | 25 | 26 |

27 |
28 | Go Home 29 |
30 | 33 |
34 | 35 |
36 | 37 | 38 |
39 |
40 |

What next?

41 |
42 | The docs 43 |

The docs

44 |

Check out the guides, try the walk-throughs, or read the API docs.

45 | Read the docs 46 |
47 |
48 | The docs 49 |

Contribute

50 |

Add your favorite new features, report issues, or squash some bugs.

51 | Visit GitHub page 52 |
53 |
54 | Get in touch 55 |

Get in touch

56 |

Come say "Hi!" -- join Genie's community on Gitter.

57 | Go to Gitter 58 |
59 |
60 |
61 | 62 | 63 | 64 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/session_adapters/FileSession.jl: -------------------------------------------------------------------------------- 1 | module FileSession 2 | 3 | using Genie 4 | import Serialization, Logging 5 | 6 | SESSIONS_PATH = "sessions" 7 | 8 | function sessions_path(path::String) 9 | SESSIONS_PATH = normpath(path) |> abspath 10 | end 11 | function sessions_path() 12 | SESSIONS_PATH 13 | end 14 | 15 | 16 | """ 17 | write(session::Genie.Sessions.Session) :: Genie.Sessions.Session 18 | 19 | Persists the `Session` object to the file system, using the configured sessions folder and returns it. 20 | """ 21 | function write(session::Genie.Sessions.Session) :: Genie.Sessions.Session 22 | if ! isdir(sessions_path()) 23 | @warn "Sessions folder $(sessions_path()) does not exist" 24 | @info "Creating sessions folder at $(sessions_path())" 25 | 26 | try 27 | mkpath(sessions_path()) 28 | catch ex 29 | @error "Can't create session storage path $(sessions_path())" 30 | @error ex 31 | end 32 | end 33 | 34 | try 35 | write_session(session) 36 | 37 | return session 38 | catch ex 39 | @error "Failed to store session data" 40 | @error ex 41 | end 42 | 43 | try 44 | @warn "Resetting session" 45 | 46 | session = Genie.Sessions.Session(Genie.Sessions.id()) 47 | Genie.Cookies.set!(Genie.Router.params(Genie.PARAMS_RESPONSE_KEY), Genie.config.session_key_name, session.id, Genie.config.session_options) 48 | write_session(session) 49 | Genie.Router.params(Genie.PARAMS_SESSION_KEY, session) 50 | 51 | return session 52 | catch ex 53 | @error "Failed to regenerate and store session data. Giving up." 54 | @error ex 55 | end 56 | 57 | session 58 | end 59 | 60 | 61 | function write_session(session::Genie.Sessions.Session) 62 | open(joinpath(SESSIONS_PATH, session.id), "w") do io 63 | Serialization.serialize(io, session) 64 | end 65 | end 66 | 67 | 68 | """ 69 | read(session_id::Union{String,Symbol}) :: Union{Nothing,Genie.Sessions.Session} 70 | read(session::Genie.Sessions.Session) :: Union{Nothing,Genie.Sessions.Session} 71 | 72 | Attempts to read from file the session object serialized as `session_id`. 73 | """ 74 | function read(session_id::Union{String,Symbol}) :: Union{Nothing,Genie.Sessions.Session} 75 | isfile(joinpath(SESSIONS_PATH, session_id)) || return nothing 76 | 77 | try 78 | open(joinpath(SESSIONS_PATH, session_id), "r") do (io) 79 | Serialization.deserialize(io) 80 | end 81 | catch ex 82 | @error "Can't read session" 83 | @error ex 84 | end 85 | end 86 | 87 | function read(session::Genie.Sessions.Session) :: Union{Nothing,Genie.Sessions.Session} 88 | read(session.id) 89 | end 90 | 91 | #===# 92 | # IMPLEMENTATION 93 | 94 | """ 95 | persist(s::Session) :: Session 96 | 97 | Generic method for persisting session data - delegates to the underlying `SessionAdapter`. 98 | """ 99 | function Genie.Sessions.persist(req::Genie.Sessions.HTTP.Request, res::Genie.Sessions.HTTP.Response, params::Dict{Symbol,Any}) :: Tuple{Genie.Sessions.HTTP.Request,Genie.Sessions.HTTP.Response,Dict{Symbol,Any}} 100 | write(params[Genie.PARAMS_SESSION_KEY]) 101 | 102 | req, res, params 103 | end 104 | function Genie.Sessions.persist(s::Genie.Sessions.Session) :: Genie.Sessions.Session 105 | write(s) 106 | end 107 | 108 | 109 | """ 110 | load(session_id::String) :: Session 111 | 112 | Loads session data from persistent storage. 113 | """ 114 | function Genie.Sessions.load(session_id::String) :: Genie.Sessions.Session 115 | session = read(session_id) 116 | 117 | session === nothing ? Genie.Sessions.Session(session_id) : (session) 118 | end 119 | 120 | end 121 | -------------------------------------------------------------------------------- /test/tests_html_rendering.jl: -------------------------------------------------------------------------------- 1 | @safetestset "HTML rendering" begin 2 | using Genie, Genie.Renderer.Html, Genie.Requests 3 | 4 | greeting = "Welcome" 5 | name = "Genie" 6 | 7 | function htmlviewfile() 8 | " 9 |

$greeting

10 |
11 |

This is a $name test

12 |
13 |
14 | " 15 | end 16 | 17 | function htmltemplatefile() 18 | " 19 | 20 | 21 | 22 | $name test 23 | 24 | 25 |
26 | <% @yield %> 27 |
28 |
Just a footer
29 | 30 | 31 | " 32 | end 33 | 34 | 35 | @testset "HTML Rendering" begin 36 | using Genie, Genie.Renderer.Html, Genie.Requests 37 | 38 | @testset "WebRenderable constructors" begin 39 | using Genie, Genie.Renderer.Html, Genie.Requests 40 | 41 | wr = Genie.Renderer.WebRenderable("hello") 42 | @test wr.body == "hello" 43 | @test wr.content_type == Genie.Renderer.DEFAULT_CONTENT_TYPE 44 | @test wr.status == 200 45 | @test wr.headers == Genie.Renderer.HTTPHeaders() 46 | 47 | wr = Genie.Renderer.WebRenderable("hello", :json) 48 | @test wr.body == "hello" 49 | @test wr.content_type == :json 50 | @test wr.status == 200 51 | @test wr.headers == Genie.Renderer.HTTPHeaders() 52 | 53 | wr = Genie.Renderer.WebRenderable() 54 | @test wr.body == "" 55 | @test wr.content_type == Genie.Renderer.DEFAULT_CONTENT_TYPE 56 | @test wr.status == 200 57 | @test wr.headers == Genie.Renderer.HTTPHeaders() 58 | 59 | wr = Genie.Renderer.WebRenderable(body = "bye", content_type = :javascript, status = 301, headers = Genie.Renderer.HTTPHeaders("Location" => "/bye")) 60 | @test wr.body == "bye" 61 | @test wr.content_type == :javascript 62 | @test wr.status == 301 63 | @test wr.headers["Location"] == "/bye" 64 | 65 | wr = Genie.Renderer.WebRenderable(Genie.Renderer.WebRenderable(body = "good morning", content_type = :javascript), 302, Genie.Renderer.HTTPHeaders("Location" => "/morning")) 66 | @test wr.body == "good morning" 67 | @test wr.content_type == :javascript 68 | @test wr.status == 302 69 | @test wr.headers["Location"] == "/morning" 70 | end; 71 | 72 | @testset "String HTML rendering" begin 73 | using Genie, Genie.Renderer.Html, Genie.Requests 74 | import Genie.Util: fws 75 | 76 | r = Requests.HTTP.Response() 77 | 78 | @testset "String no layout" begin 79 | rm("build", force = true, recursive = true) 80 | r = html(htmlviewfile(), forceparse = true) 81 | 82 | @test String(r.body) |> fws == 83 | "

$greeting

This is a $name test

84 | " |> fws 85 | end; 86 | 87 | @testset "String with layout" begin 88 | rm("build", force = true, recursive = true) 89 | r = html(htmlviewfile(), layout = htmltemplatefile()) 90 | 91 | @test String(r.body) |> fws == 92 | "$name test
93 |

$greeting

This is a $name test

94 |
Just a footer
" |> fws 95 | end; 96 | 97 | @test r.status == 200 98 | @test r.headers[1]["Content-Type"] == "text/html; charset=utf-8" 99 | 100 | rm("build", force = true, recursive = true) 101 | end; 102 | end; 103 | end -------------------------------------------------------------------------------- /docs/src/tutorials/10--Loading_Genie_Apps.md: -------------------------------------------------------------------------------- 1 | # Loading and starting Genie apps 2 | 3 | At any time, you can load and serve an existing Genie app. Loading a Genie app will bring into scope all your app's files, including the main app module, controllers, models, etcetera. 4 | 5 | ## Starting a Genie REPL session MacOS / Linux 6 | 7 | The recommended approach is to start an interactive REPL in Genie's environment by executing `bin/repl` in the os shell, while in the project's root folder. 8 | 9 | ```sh 10 | $ bin/repl 11 | ``` 12 | 13 | The app's environment will be loaded. 14 | 15 | In order to start the web server, you can next execute: 16 | 17 | ```julia 18 | julia> up() 19 | ``` 20 | 21 | If you want to directly start the server, use `bin/server` instead of `bin/repl`: 22 | 23 | ```sh 24 | $ bin/server 25 | ``` 26 | 27 | This will automatically start the web server _in non interactive mode_. 28 | 29 | Finally, there is the option to start the serve and drop to an interactive REPL, using `bin/serverinteractive` instead. 30 | 31 | ## Starting a Genie REPL on Windows 32 | 33 | On Windows the workflow is similar to macOS and Linux, but dedicated Windows scripts, `repl.bat`, `server.bat`, and `serverinteractive.bat` are provided inside the project folder, within the `bin/` directory. Double click them or execute them in the os shell (cmd or PowerShell) to start an interactive REPL session or a server session, respectively, as explained in the previous paragraphs. 34 | 35 | --- 36 | **HEADS UP** 37 | 38 | It is possible that the Windows executables `repl.bat`, `server.bat`, and `serverinteractive.bat` are missing - this is usually the case if the app was generated on a Linux/Mac and ported to a windows computer. You can create them at anytime by running this generator in the Genie/Julia REPL (at the root of the Genie project): 39 | 40 | ```julia 41 | julia> using Genie 42 | 43 | julia> Genie.Generator.setup_windows_bin_files() 44 | ``` 45 | 46 | Alternatively, you can pass the path to the project as the argument to `setup_windows_bin_files`: 47 | 48 | ```julia 49 | julia> Genie.Generator.setup_windows_bin_files("path/to/your/Genie/project") 50 | ``` 51 | 52 | ## Juno / Jupyter / other Julia environment 53 | 54 | For Juno, Jupyter, and other interactive environments, first make sure that you `cd` into your app's project folder. 55 | 56 | We will need to make the local package environment available: 57 | 58 | ```julia 59 | using Pkg 60 | Pkg.activate(".") 61 | ``` 62 | 63 | Then: 64 | 65 | ```julia 66 | using Genie 67 | 68 | Genie.loadapp() 69 | ``` 70 | 71 | ## Manual loading in Julia's REPL 72 | 73 | In order to load a Genie app within an open Julia REPL session, first make sure that you're in the root dir of a Genie app. This is the project's folder and you can tell by the fact that there should be a `bootstrap.jl` file, plus Julia's `Project.toml` and `Manifest.toml` files, amongst others. You can `julia> cd(...)` or `shell> cd ...` your way into the folder of the Genie app. 74 | 75 | Next, from within the active Julia REPL session, we have to activate the local package environment: 76 | 77 | ```julia 78 | julia> ] # enter pkg> mode 79 | 80 | pkg> activate . 81 | ``` 82 | 83 | Then, back to the julian prompt, run the following to load the Genie app: 84 | 85 | ```julia 86 | julia> using Genie 87 | 88 | julia> Genie.loadapp() 89 | ``` 90 | 91 | The app's environment will now be loaded. 92 | 93 | In order to start the web server execute 94 | 95 | ```julia 96 | julia> startup() 97 | ``` 98 | 99 | --- 100 | **HEADS UP** 101 | 102 | The recommended way to load an app is via the `bin/repl`, `bin/server` and `bin/serverinteractive` commands. It will correctly start the Julia process and start the app REPL with all the dependencies loaded with just one command. 103 | 104 | --- 105 | -------------------------------------------------------------------------------- /docs/src/tutorials/16--Using_Genie_With_Docker.md: -------------------------------------------------------------------------------- 1 | # Using Genie with Docker 2 | 3 | Genie comes with built-in support for containerizing apps. The functionality is available in the `Genie.Deploy.Docker` module. 4 | 5 | ## Generating the Genie-optimised `Dockerfile` 6 | 7 | You can bootstrap the Docker setup by invoking the `Genie.Deploy.Docker.dockerfile()` function. This will generate a custom `Dockerfile` optimized for Genie web apps containerization. The file will be generated in the current work dir (or where instructed by the optional argument `path` -- see the help for the `dockerfile()` function). Once generated, you can edit it and customize it as needed - Genie will not overwrite the file, thus preserving any changes. 8 | 9 | The behaviour of `dockerfile()` can be controlled by passing any of the multiple optional arguments supported. 10 | 11 | ## Building the Docker container 12 | 13 | Once we have our `Dockerfile` ready, we can invoke `Genie.Deploy.Docker.build()` in order to build the Docker container. You can optionally pass the container's name (by default `"genie"`) and the path (defaults to current work dir). 14 | 15 | ## Running the Genie app within the Docker container 16 | 17 | When the image is ready, we can run it with `Genie.Deploy.Docker.run()`. We can configure any of the optional arguments in order to control how the app is run. Check the inline help for the function for more details. 18 | 19 | ## Examples 20 | 21 | First let's create a Genie app: 22 | 23 | ```julia 24 | julia> using Genie 25 | 26 | julia> Genie.newapp("DockerTest") 27 | [ Info: Done! New app created at /Users/adrian/DockerTest 28 | # output truncated 29 | ``` 30 | 31 | When it's ready, let's add the `Dockerfile`: 32 | 33 | ```julia 34 | julia> using Genie.Deploy 35 | 36 | julia> Deploy.Docker.dockerfile() 37 | Docker file successfully written at /Users/adrian/DockerTest/Dockerfile 38 | ``` 39 | 40 | Now, to build our container: 41 | 42 | ```julia 43 | julia> Deploy.Docker.build() 44 | Sending build context to Docker daemon 1.056MB 45 | Step 1/18 : FROM julia:latest 46 | ---> f4c9686d85da 47 | # output truncated 48 | Successfully tagged genie:latest 49 | Docker container successfully built 50 | ``` 51 | 52 | And finally, we can now run our app within the Docker container: 53 | 54 | ```julia 55 | julia> Deploy.Docker.run() 56 | Starting docker container with `docker run -it --rm -p 80:8000 --name genieapp genie bin/server` 57 | 58 | _____ _ 59 | | __|___ ___|_|___ 60 | | | | -_| | | -_| 61 | |_____|___|_|_|_|___| 62 | 63 | | Web: https://genieframework.com 64 | | GitHub: https://github.com/genieframework/Genie.jl 65 | | Docs: https://genieframework.github.io/Genie.jl 66 | | Gitter: https://gitter.im/essenciary/Genie.jl 67 | | Twitter: https://twitter.com/GenieMVC 68 | 69 | Genie v0.19.0 70 | Active env: DEV 71 | 72 | Web Server starting at http://127.0.0.1:8000 73 | ``` 74 | 75 | Our application starts inside the Docker container, binding port 8000 within the container (where the Genie app is running) to the port 80 of the host. So we are now able to access our app at `http://localhost`. If you navigate to `http://localhost` with your favourite browser you'll see Genie's welcome page. Notice that we don't access on port 8000 - this page is served from the Docker container on the default port 80. 76 | 77 | ### Using Docker during development 78 | 79 | If we want to use Docker to serve the app during development, we need to _mount_ our app from host (your computer) into the container -- so that we can keep editing our files locally, but see the changes reflected in the Docker container. In order to do this we need to pass the `mountapp = true` argument to `Deploy.Docker.run()`, like this: 80 | 81 | ```julia 82 | julia> Deploy.Docker.run(mountapp = true) 83 | Starting docker container with `docker run -it --rm -p 80:8000 --name genieapp -v /Users/adrian/DockerTest:/home/genie/app genie bin/server` 84 | ``` 85 | 86 | When the app finishes starting, we can edit the files on the host using our favourite IDE, and see the changes reflected in the Docker container. 87 | -------------------------------------------------------------------------------- /docs/src/tutorials/3--Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Hello world with Genie 2 | 3 | Here are a few examples to quickly get you started with building Genie web apps. 4 | 5 | ## Running Genie interactively at the REPL or in Jupyter 6 | 7 | The simplest use case is to configure a routing function at the REPL and start the web server. That's all that's needed to run your code on the web: 8 | 9 | ### Example 10 | 11 | ```julia 12 | julia> using Genie, Genie.Router 13 | 14 | julia> route("/hello") do 15 | "Hello World" 16 | end 17 | 18 | julia> up() 19 | ``` 20 | 21 | The `route` function (available in the `Router` module) defines a mapping between a URL (`"/hello"`) and a Julia function which will be automatically invoked to send the response back to the client. In this case we're sending back the string "Hello World". 22 | 23 | That's all! We have set up an app, a route, and started the web server. Open your favourite web browser and go to to see the result. 24 | 25 | --- 26 | **HEADS UP** 27 | 28 | Keep in mind that Julia JIT-compiles. A function is automatically compiled the first time it is invoked. The function, in this case, is our route handler serving the request. This will make the first response slower as it also includes compilation time. But once the function is compiled, for all the subsequent requests, it will be super fast! 29 | 30 | --- 31 | 32 | ## Developing a simple Genie script 33 | 34 | Genie can also be used in custom scripts, for example when building micro-services with Julia. Let's create a simple Hello World micro-service. 35 | 36 | Start by creating a new file to host our code -- let's call it `geniews.jl` 37 | 38 | ```julia 39 | julia> touch("geniews.jl") 40 | ``` 41 | 42 | Now, open it in the editor: 43 | 44 | ```julia 45 | julia> edit("geniews.jl") 46 | ``` 47 | 48 | Add the following code: 49 | 50 | ```julia 51 | using Genie, Genie.Router 52 | using Genie.Renderer, Genie.Renderer.Html, Genie.Renderer.Json 53 | 54 | route("/hello.html") do 55 | html("Hello World") 56 | end 57 | 58 | route("/hello.json") do 59 | json("Hello World") 60 | end 61 | 62 | route("/hello.txt") do 63 | respond("Hello World", :text) 64 | end 65 | 66 | up(8001, async = false) 67 | ``` 68 | 69 | We begun by defining 2 routes and we used the `html` and `json` rendering functions (available in the `Renderer.Html` and the `Renderer.Json` modules). These functions are responsible for outputting the data using the correct format and document type (with the correct MIME), in our case HTML data for `hello.html`, and JSON data for `hello.json`. 70 | 71 | The third `route` serves text responses. As Genie does not provide a specialized method for sending `text/plain` responses, we use the generic `respond` function, indicating the desired MIME type. In our case `:text`, corresponding to `text/plain`. Other available MIME types shortcuts are `:xml`, `:markdown`, and `:javascript`. If you're looking for something else, you can always pass the full mime type as a string, ie `"text/csv"`. 72 | 73 | The `up` function will launch the web server on port `8001`. This time, very important, we instructed it to start the server synchronously (that is, _blocking_ the execution of the script), by passing the `async = false` argument. This way we make sure that our script stays running. Otherwise, at the end of the script, it would normally exit, killing our server. 74 | 75 | In order to launch the script, run `$ julia geniews.jl`. 76 | 77 | ## Batteries included 78 | 79 | Genie readily makes available a rich set of features - you have already seen the rendering and the routing engines in action. But for instance, logging (to file and console) can also be easily triggered with one line of code, powerful caching can be enabled with a couple more lines, and so on. 80 | 81 | The app already handles "404 Page Not Found" and "500 Internal Error" responses. If you try to access a URL which is not handled by the app, like say , you'll see Genie's default 404 page. The default error pages can be overwritten with custom ones and we'll see how to do this later on. 82 | -------------------------------------------------------------------------------- /files/new_app/public/error-500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Genie :: The highly productive Julia web framework 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 |
20 |

500 Internal Server Error

21 |

22 | 23 | 24 | 25 |

26 |
27 | Go Home 28 |
29 | 32 |
33 |
34 | 35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
 43 |             
 44 | 
 45 |             
 46 |           
47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 |

What next?

57 |
58 | The docs 59 |

The docs

60 |

Check out the guides, try the walk-throughs, or read the API docs.

61 | Read the docs 62 |
63 |
64 | The docs 65 |

Contribute

66 |

Add your favorite new features, report issues, or squash some bugs.

67 | Visit GitHub page 68 |
69 |
70 | Get in touch 71 |

Get in touch

72 |

Come say "Hi!" -- join Genie's community on Gitter.

73 | Go to Gitter 74 |
75 |
76 |
77 | 78 | 79 | 80 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/FileTemplates.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Functionality for handling the defautl conent of the various Genie files (migrations, models, controllers, etc). 3 | """ 4 | module FileTemplates 5 | 6 | import Inflector 7 | 8 | 9 | """ 10 | newtask(module_name::String) :: String 11 | 12 | Default content for a new Genie Toolbox task. 13 | """ 14 | function newtask(module_name::String) :: String 15 | """ 16 | module $module_name 17 | 18 | \"\"\" 19 | Description of the task here 20 | \"\"\" 21 | function runtask() 22 | # Build something great 23 | end 24 | 25 | end 26 | """ 27 | end 28 | 29 | 30 | """ 31 | newcontroller(controller_name::String) :: String 32 | 33 | Default content for a new Genie controller. 34 | """ 35 | function newcontroller(controller_name::String) :: String 36 | """ 37 | module $(controller_name)Controller 38 | # Build something great 39 | end 40 | """ 41 | end 42 | 43 | 44 | """ 45 | newtest(plural_name::String, singular_name::String) :: String 46 | 47 | Default content for a new test file. 48 | """ 49 | function newtest(plural_name::String, singular_name::String) :: String 50 | """ 51 | using Genie, App.$(plural_name) 52 | 53 | ### Your tests here 54 | @test 1 == 1 55 | """ 56 | end 57 | 58 | 59 | """ 60 | appmodule(path::String) 61 | 62 | Generates a custom app module when a new app is bootstrapped. 63 | """ 64 | function appmodule(path::String) 65 | path = replace(path, '-'=>'_') |> strip 66 | appname = split(path, '/', keepempty = false)[end] |> String |> Inflector.from_underscores 67 | 68 | content = """ 69 | module $appname 70 | 71 | using Genie, Logging, LoggingExtras 72 | 73 | function main() 74 | Core.eval(Main, :(const UserApp = \$(@__MODULE__))) 75 | 76 | Genie.genie(; context = @__MODULE__) 77 | 78 | Core.eval(Main, :(const Genie = UserApp.Genie)) 79 | Core.eval(Main, :(using Genie)) 80 | end 81 | 82 | end 83 | """ 84 | 85 | (appname, content) 86 | end 87 | 88 | 89 | """ 90 | dockerfile(; user::String = "genie", supervisor::Bool = false, nginx::Bool = false, env::String = "dev", 91 | filename::String = "Dockerfile", port::Int = 8000, dockerport::Int = 80, host::String = "0.0.0.0", 92 | websockets_port::Int = port, websockets_dockerport::Int = dockerport) 93 | 94 | Generates dockerfile for the Genie app. 95 | """ 96 | function dockerfile(; user::String = "genie", supervisor::Bool = false, nginx::Bool = false, env::String = "dev", 97 | filename::String = "Dockerfile", port::Int = Genie.config.server_port, dockerport::Int = 80, 98 | host::String = "0.0.0.0", websockets_port::Int = port, platform::String = "", 99 | websockets_dockerport::Int = dockerport, earlybind::Bool = true) 100 | appdir = "/home/$user/app" 101 | 102 | string( 103 | """ 104 | # pull latest julia image 105 | FROM $(isempty(platform) ? "" : "--platform=$platform") julia:latest 106 | 107 | # create dedicated user 108 | RUN useradd --create-home --shell /bin/bash $user 109 | 110 | # set up the app 111 | RUN mkdir $appdir 112 | COPY . $appdir 113 | WORKDIR $appdir 114 | 115 | # configure permissions 116 | RUN chown $user:$user -R * 117 | 118 | RUN chmod +x bin/repl 119 | RUN chmod +x bin/server 120 | RUN chmod +x bin/runtask 121 | 122 | # switch user 123 | USER $user 124 | 125 | # instantiate Julia packages 126 | RUN julia -e "using Pkg; Pkg.activate(\\".\\"); Pkg.instantiate(); Pkg.precompile(); " 127 | 128 | # ports 129 | EXPOSE $port 130 | EXPOSE $dockerport 131 | """, 132 | 133 | (websockets_port != port ? 134 | """ 135 | 136 | # websockets ports 137 | EXPOSE $websockets_port 138 | EXPOSE $websockets_dockerport 139 | """ : ""), 140 | 141 | """ 142 | 143 | # set up app environment 144 | ENV JULIA_DEPOT_PATH "/home/$user/.julia" 145 | ENV GENIE_ENV "$env" 146 | ENV HOST "$host" 147 | ENV PORT "$port" 148 | ENV WSPORT "$websockets_port" 149 | ENV EARLYBIND "$earlybind" 150 | """, 151 | 152 | """ 153 | 154 | # run app 155 | CMD ["bin/server"] 156 | 157 | # or maybe include a Julia file 158 | # CMD julia -e 'using Pkg; Pkg.activate("."); include("IrisClustering.jl"); ' 159 | """) 160 | end 161 | 162 | end 163 | -------------------------------------------------------------------------------- /test/tests_basic_rendering.jl: -------------------------------------------------------------------------------- 1 | @safetestset "basic rendering" begin 2 | using Genie 3 | using Genie.Renderer.Html 4 | using Genie.Requests 5 | import Genie.Util: fws 6 | 7 | content = "abcd" 8 | content2 = "efgh" 9 | 10 | @testset "Basic rendering" begin 11 | r = Requests.HTTP.Response() 12 | 13 | 14 | @testset "Empty string" begin 15 | r = html("") 16 | 17 | @test String(r.body) == "" 18 | end; 19 | 20 | # @testset "Empty string force parse" begin 21 | # @test_throws ArgumentError html("", forceparse = true) 22 | # end; 23 | 24 | 25 | @testset "String no spaces" begin 26 | r = html(content) 27 | 28 | @test String(r.body) == "$content" 29 | end; 30 | 31 | @testset "String no spaces force parse" begin 32 | r = html(content, forceparse = true) 33 | 34 | @test String(r.body) |> fws == "

abcd

" |> fws 35 | end; 36 | 37 | 38 | @testset "String with 2 spaces" begin 39 | r = html(" $content ") 40 | 41 | @test String(r.body) == " abcd " 42 | end; 43 | 44 | @testset "String with 2 spaces force parse" begin 45 | r = html(" $content ", forceparse = true) 46 | 47 | @test String(r.body) |> fws == "

abcd

" |> fws 48 | end; 49 | 50 | 51 | @testset "String with  " begin 52 | r = html("    ") 53 | 54 | @test String(r.body) == "    " 55 | end; 56 | 57 | @testset "String with   force parse" begin 58 | r = html("    ", forceparse = true) 59 | 60 | @test String(r.body) |> fws == "

  

" |> fws 61 | end; 62 | 63 | 64 | @testset "String with 2   and 2 spaces" begin 65 | r = html("  $content ") 66 | 67 | @test String(r.body) == "  abcd " 68 | end; 69 | 70 | @testset "String with 2   and 2 spaces force parse" begin 71 | r = html("  $content ", forceparse = true) 72 | 73 | @test String(r.body) |> fws == "

  abcd

" |> fws 74 | end; 75 | 76 | 77 | @testset "String with newline" begin 78 | r = html("$content \n   $content2") 79 | 80 | @test String(r.body) == "abcd \n   efgh" 81 | end; 82 | 83 | @testset "String with newline force parse" begin 84 | r = html("$content \n   $content2", forceparse = true) 85 | 86 | @test String(r.body) |> fws == "

abcd \n  efgh

" |> fws 87 | end; 88 | 89 | 90 | @testset "String with quotes" begin 91 | r = html("He said \"wow!\"") 92 | 93 | @test String(r.body) == "He said \"wow!\"" 94 | end; 95 | 96 | @testset "String with quotes force parse" begin 97 | r = html("He said \"wow!\"", forceparse = true) 98 | 99 | @test String(r.body) |> fws == "

He said \"wow!\"\0

" |> fws 100 | end; 101 | 102 | 103 | @testset "String with quotes" begin 104 | r = html(""" "" """) 105 | 106 | @test String(r.body) == " \"\" " 107 | end; 108 | 109 | @testset "String with quotes force parse" begin 110 | r = html(""" "" """, forceparse = true) 111 | 112 | @test String(r.body) |> fws == "

\"\"

" |> fws 113 | end; 114 | 115 | 116 | @testset "String with quotes" begin 117 | r = html("\"\"") 118 | 119 | @test String(r.body) == "\"\"" 120 | end; 121 | 122 | @testset "String with quotes force parse" begin 123 | r = html("\"\"", forceparse = true) 124 | 125 | @test String(r.body) |> fws == "

\"\"\0

" |> fws 126 | end; 127 | 128 | 129 | @testset "String with interpolated vars" begin 130 | r = html("$(reverse(content))") 131 | 132 | @test String(r.body) == "dcba" 133 | end; 134 | 135 | @testset "String with interpolated vars force parse" begin 136 | r = html("$(reverse(content))", forceparse = true) 137 | 138 | @test String(r.body) |> fws == "

dcba

" |> fws 139 | end; 140 | 141 | 142 | @test r.status == 200 143 | @test r.headers[1]["Content-Type"] == "text/html; charset=utf-8" 144 | end; 145 | end -------------------------------------------------------------------------------- /test/tests_genie_generators.jl: -------------------------------------------------------------------------------- 1 | #= 2 | 3 | @safetestset "Create new app" begin 4 | 5 | testdir = pwd() 6 | using Pkg 7 | 8 | 9 | @safetestset "Do not autostart app" begin 10 | using Genie 11 | 12 | workdir = Base.Filesystem.mktempdir() 13 | 14 | cd(workdir) 15 | 16 | Genie.newapp(workdir, autostart = false, testmode = true) 17 | 18 | @test true === true 19 | end; 20 | 21 | 22 | # cd(testdir) 23 | # Pkg.activate(".") 24 | 25 | 26 | # @safetestset "Autostart app" begin 27 | # using Genie 28 | 29 | # workdir = Base.Filesystem.mktempdir() 30 | 31 | # Genie.newapp(workdir, autostart = true, testmode = true) 32 | 33 | # @test true === true 34 | # end; 35 | 36 | 37 | cd(testdir) 38 | Pkg.activate(".") 39 | 40 | 41 | @safetestset "Microstack file structure" begin 42 | using Genie 43 | 44 | workdir = Base.Filesystem.mktempdir() 45 | 46 | cd(workdir) 47 | 48 | Genie.newapp(workdir, autostart = false, testmode = true) 49 | 50 | @test sort(readdir(workdir)) == sort([".gitattributes", ".gitignore", "Manifest.toml", "Project.toml", "bin", 51 | "bootstrap.jl", "config", "genie.jl", "public", "routes.jl", "src"]) 52 | @test readdir(joinpath(workdir, Genie.config.path_initializers)) == ["autoload.jl", "converters.jl", "logging.jl", "ssl.jl"] 53 | 54 | # TODO: add test for files in /src /config /public and /bin 55 | end; 56 | 57 | 58 | cd(testdir) 59 | Pkg.activate(".") 60 | 61 | 62 | @safetestset "DB support file structure" begin 63 | using Genie 64 | 65 | workdir = Base.Filesystem.mktempdir() 66 | 67 | cd(workdir) 68 | 69 | Genie.newapp(workdir, autostart = false, dbsupport = true, testmode = true) 70 | 71 | @test sort(readdir(workdir)) == sort([".gitattributes", ".gitignore", "Manifest.toml", "Project.toml", "bin", 72 | "bootstrap.jl", "config", "db", "genie.jl", "public", "routes.jl", "src"]) 73 | @test sort(readdir(joinpath(workdir, Genie.config.path_db))) == sort(["connection.yml", "migrations", "seeds"]) 74 | @test sort(readdir(joinpath(workdir, Genie.config.path_initializers))) == sort(["autoload.jl", "converters.jl", "logging.jl", "searchlight.jl", "ssl.jl"]) 75 | end; 76 | 77 | 78 | cd(testdir) 79 | Pkg.activate(".") 80 | 81 | 82 | @safetestset "MVC support file structure" begin 83 | using Genie 84 | 85 | workdir = Base.Filesystem.mktempdir() 86 | 87 | cd(workdir) 88 | 89 | Genie.newapp(workdir, autostart = false, mvcsupport = true, testmode = true) 90 | 91 | @test sort(readdir(workdir)) == sort([".gitattributes", ".gitignore", "Manifest.toml", "Project.toml", "app", "bin", "bootstrap.jl", "config", "genie.jl", "public", "routes.jl", "src"]) 92 | @test sort(readdir(joinpath(workdir, Genie.config.path_app))) == sort(["helpers", "layouts", "resources"]) 93 | @test sort(readdir(joinpath(workdir, Genie.config.path_initializers))) == sort(["autoload.jl", "converters.jl", "logging.jl", "ssl.jl"]) 94 | end; 95 | 96 | 97 | cd(testdir) 98 | Pkg.activate(".") 99 | 100 | 101 | @safetestset "New controller" begin 102 | using Genie 103 | 104 | workdir = Base.Filesystem.mktempdir() 105 | 106 | cd(workdir) 107 | 108 | Genie.Generator.newcontroller("Yazoo") 109 | 110 | @test isdir(joinpath(workdir, "app", "resources", "yazoo")) == true 111 | @test isfile(joinpath(workdir, "app", "resources", "yazoo", "YazooController.jl")) == true 112 | end; 113 | 114 | cd(testdir) 115 | Pkg.activate(".") 116 | 117 | 118 | @safetestset "New resource" begin 119 | using Genie 120 | 121 | workdir = Base.Filesystem.mktempdir() 122 | 123 | cd(workdir) 124 | 125 | Genie.newresource("Kazoo") 126 | 127 | @test isdir(joinpath(workdir, "app", "resources", "kazoo")) == true 128 | @test isfile(joinpath(workdir, "app", "resources", "kazoo", "KazooController.jl")) == true 129 | end; 130 | 131 | 132 | cd(testdir) 133 | Pkg.activate(".") 134 | 135 | 136 | @safetestset "New task" begin 137 | using Genie, Genie.Exceptions 138 | 139 | workdir = Base.Filesystem.mktempdir() 140 | 141 | cd(workdir) 142 | 143 | Genie.newtask("Vavoom") 144 | 145 | @test isdir(joinpath(workdir, "tasks")) == true 146 | @test isfile(joinpath(workdir, "tasks", "VavoomTask.jl")) == true 147 | @test_throws FileExistsException Genie.newtask("Vavoom") 148 | end; 149 | 150 | 151 | cd(testdir) 152 | Pkg.activate(".") 153 | 154 | 155 | end; 156 | 157 | =# -------------------------------------------------------------------------------- /src/Plugins.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Functionality for creating and working with Genie plugins. 3 | """ 4 | module Plugins 5 | 6 | import Genie 7 | import Pkg, Markdown, Logging 8 | 9 | 10 | const FILES_FOLDER = "files" 11 | const PLUGINS_FOLDER = Genie.config.path_plugins 12 | const TASKS_FOLDER = Genie.config.path_tasks 13 | const APP_FOLDER = Genie.config.path_app 14 | 15 | const path_prefix = joinpath(@__DIR__, "..", FILES_FOLDER, "new_app") |> normpath |> relpath 16 | const FOLDERS = [ joinpath(path_prefix, APP_FOLDER), 17 | joinpath(path_prefix, "db"), 18 | joinpath(path_prefix, PLUGINS_FOLDER), 19 | joinpath(path_prefix, Genie.config.server_document_root) ] 20 | 21 | 22 | """ 23 | recursive_copy(path::String, dest::String; only_hidden = true, force = false) 24 | 25 | Utility function to copy plugin files from package dir to app. 26 | """ 27 | function recursive_copy(path::String, dest::String; only_hidden = true, force = false) 28 | for (root, dirs, files) in walkdir(path) 29 | dest_path = joinpath(dest, replace(root, (path_prefix * '/')=>"")) 30 | 31 | try 32 | mkpath(dest_path) 33 | @info "Created dir $dest_path" 34 | catch ex 35 | @error "Failed to create dir $dest_path" 36 | end 37 | 38 | for f in files 39 | (only_hidden && startswith(f, ".")) || continue # only copy hidden files, especially .gitkeep 40 | try 41 | cp(joinpath(root, f), joinpath(dest_path, f), force = force) 42 | @info "Copied $(joinpath(root, f)) to $(joinpath(dest_path, f))" 43 | catch ex 44 | @error "Failed to copy $(joinpath(root, f)) to $(joinpath(dest_path, f))" 45 | end 46 | end 47 | end 48 | end 49 | 50 | 51 | """ 52 | congrats() 53 | 54 | Shows success message and instructions when scaffolding a plugin. 55 | """ 56 | function congrats() 57 | message = """ 58 | Congratulations, your plugin is ready! 59 | You can use this default installation function in your plugin's module: 60 | """ 61 | print(message) 62 | 63 | Markdown.doc""" 64 | ```julia 65 | function install(dest::String; force = false) :: Nothing 66 | src = abspath(normpath(joinpath(@__DIR__, "..", Genie.Plugins.FILES_FOLDER))) 67 | 68 | for f in readdir(src) 69 | isfile(f) && continue 70 | isdir(f) || mkpath(joinpath(src, f)) 71 | 72 | Genie.Plugins.install(joinpath(src, f), dest, force = force) 73 | end 74 | 75 | nothing 76 | end 77 | ``` 78 | """ 79 | end 80 | 81 | 82 | """ 83 | scaffold(plugin_name::String, dest::String = "."; force = false) 84 | 85 | Scaffolds a new plugin as a Julia project 86 | """ 87 | function scaffold(plugin_name::String, dest::String = "."; force = false) 88 | plugin_name = replace(plugin_name, " "=>"") |> strip |> string 89 | dest = normpath(dest) |> abspath 90 | ispath(dest) || mkpath(dest) 91 | 92 | @info "Generating project file" 93 | cd(dest) 94 | Pkg.generate(plugin_name) 95 | dest = joinpath(dest, plugin_name) 96 | 97 | @info "Scaffolding file structure" 98 | mkpath(joinpath(dest, FILES_FOLDER)) 99 | 100 | for path in FOLDERS 101 | recursive_copy(path, joinpath(dest, FILES_FOLDER), force = force) 102 | end 103 | 104 | initializer_path = joinpath(dest, FILES_FOLDER, PLUGINS_FOLDER, lowercase(plugin_name) * ".jl") 105 | @info "Creating plugin initializer at $initializer_path" 106 | touch(initializer_path) 107 | 108 | @info "Adding dependencies" 109 | 110 | cd(dest) 111 | Pkg.activate(".") 112 | Pkg.add("Genie") 113 | 114 | run(`git init`) 115 | run(`git add .`) 116 | run(`git commit -am "initial commit"`) 117 | 118 | congrats() 119 | end 120 | 121 | 122 | """ 123 | install(path::String, dest::String; force = false) 124 | 125 | Utility to allow users to install a plugin 126 | """ 127 | function install(path::String, dest::String; force = false) 128 | isdir(dest) || mkpath(dest) 129 | cd(dest) 130 | isdir(Genie.config.path_plugins) || mkpath(Genie.config.path_plugins) 131 | 132 | root_length = splitpath(path) |> length 133 | depth = 0 134 | 135 | for (root, dirs, files) in walkdir(path) 136 | depth = length(splitpath(root)) - root_length 137 | dest_path = joinpath(abspath(dest), splitpath(root)[end-depth:end]...) 138 | 139 | try 140 | mkpath(dest_path) 141 | @info "Created dir $dest_path" 142 | catch ex 143 | @error "Did not create dir $dest_path" 144 | end 145 | 146 | for f in files 147 | try 148 | cp(joinpath(root, f), joinpath(dest_path, f), force = force) 149 | @info "Copied $(joinpath(root, f)) to $(joinpath(dest_path, f))" 150 | catch ex 151 | @error "Did not copy $(joinpath(root, f)) to $(joinpath(dest_path, f))" 152 | end 153 | end 154 | end 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /docs/src/tutorials/4--Developing_Web_Services.md: -------------------------------------------------------------------------------- 1 | # Developing Genie Web Services 2 | 3 | Starting up ad-hoc web servers at the REPL and writing small scripts to wrap micro-services works great, but production apps tend to become complex very quickly. They also have more stringent requirements, like managing dependencies, compressing assets, reloading code, logging, environments, or structuring the codebase in a way which promotes efficient workflows when working in teams. 4 | 5 | Genie apps provide all these features, from dependency management and versioning (using Julia's `Pkg`, since a Genie app is a Julia project), to a powerful asset pipeline (using industry vetted tools like Yarn and Webpack), automatic code reloading in development (provided by `Revise.jl`), and a clear resource-oriented MVC layout. 6 | 7 | Genie enables a modular approach towards app building, allowing to add more components as the need arises. You can start with the web service template (which includes dependencies management, logging, environments, and routing), and grow it by sequentially adding DB persistence (through the SearchLight ORM), high performance HTML view templates with embedded Julia (via Flax), asset pipeline and compilation, and more. 8 | 9 | ## Setting up a Genie Web Service project 10 | 11 | Genie packs handy generator features and templates which help bootstrapping and setting up various parts of an application. For bootstrapping a new app we need to invoke one of the functions in the `newapp` family: 12 | 13 | ```julia 14 | julia> using Genie 15 | 16 | julia> Genie.newapp_webservice("MyGenieApp") 17 | ``` 18 | 19 | If you follow the log messages in the REPL you will see that the command will trigger a flurry of actions in order to set up the new project: 20 | 21 | - it creates a new folder, `MyGenieApp/`, which will hosts the files of the app and whose name corresponds to the name of the app, 22 | - within the `MyGenieApp/` folder, it creates the files and folders needed by the app, 23 | - changes the active directory to `MyGenieApp/` and creates a new Julia project within it (adding the `Project.toml` file), 24 | - installs all the required dependencies for the new Genie app (using `Pkg` and the standard `Manifest.toml` file), and finally, 25 | - starts the web server 26 | 27 | --- 28 | **TIP** 29 | 30 | Check out the inline help for `Genie.newapp`, `Genie.newapp_webservice`, `Genie.newapp_mvc`, and `Genie.newapp_fullstack` too see what options are available for bootstrapping applications. We'll go over the different configurations in upcoming sections. 31 | 32 | --- 33 | 34 | ## The file structure 35 | 36 | Our newly created web service has this file structure: 37 | 38 | ```julia 39 | ├── Manifest.toml 40 | ├── Project.toml 41 | ├── bin 42 | ├── bootstrap.jl 43 | ├── config 44 | ├── genie.jl 45 | ├── log 46 | ├── public 47 | ├── routes.jl 48 | └── src 49 | ``` 50 | 51 | These are the roles of each of the files and folders: 52 | 53 | - `Manifest.toml` and `Project.toml` are used by Julia and `Pkg` to manage the app's dependencies. 54 | - `bin/` includes scripts for starting up a Genie REPL or a Genie server. 55 | - `bootstrap.jl`, `genie.jl`, as well as all the files within `src/` are used by Genie to load the application and _should not be user modified_. 56 | - `config/` includes the per-environment configuration files. 57 | - `log/` is used by Genie to store per-environment log files. 58 | - `public/` is the document root, which includes static files exposed by the app on the network/internet. 59 | - `routes.jl` is the dedicated file for registering Genie routes. 60 | 61 | --- 62 | **HEADS UP** 63 | 64 | After creating a new app you might need to change the file permissions to allow editing/saving the files such as `routes.jl`. 65 | 66 | --- 67 | 68 | ## Adding logic 69 | 70 | You can now edit the `routes.jl` file to add some logic, at the bottom of the file: 71 | 72 | ```julia 73 | route("/hello") do 74 | "Welcome to Genie!" 75 | end 76 | ``` 77 | 78 | If you now visit you'll see a warm greeting. 79 | 80 | ## Growing the app 81 | 82 | Genie apps are just plain Julia projects. This means that `routes.jl` will behave like any other Julia script - you can reference extra packages, you can switch into `pkg>` mode to manage per project dependencies, include other files, etcetera. 83 | 84 | If you have existing Julia code that you want to quickly load into a Genie app, you can add a `lib/` folder in the root of the app and place your Julia files there. When available, `lib/` and all its subfolders are automatically added to the `LOAD_PATH` by Genie, recursively. 85 | 86 | If you need to add database support, you can always add the SearchLight ORM by using the dedicated generator, running `julia> Genie.Generator.db_support()` in the app's REPL. 87 | 88 | However, if your app grows in complexity and you develop it from scratch, it is more efficient to take advantage of Genie's resource-oriented MVC structure. 89 | -------------------------------------------------------------------------------- /src/Exceptions.jl: -------------------------------------------------------------------------------- 1 | module Exceptions 2 | 3 | import Genie 4 | import HTTP 5 | 6 | export ExceptionalResponse, RuntimeException, InternalServerException, NotFoundException, FileExistsException 7 | 8 | 9 | """ 10 | struct ExceptionalResponse <: Exception 11 | 12 | A type of exception which wraps an HTTP Response object. 13 | The thrown exception will propagate until it is caught up the app stack or ultimately by Genie 14 | and the wrapped response is sent to the client. 15 | 16 | ### Example 17 | If the user is not authenticated, an `ExceptionalResponse` is thrown - if the exception is not caught 18 | in the app's stack, Genie will catch it and return the wrapped `Response` object, forcing an HTTP redirect to the login page. 19 | 20 | ```julia 21 | isauthenticated() || throw(ExceptionalResponse(redirect(:show_login))) 22 | ``` 23 | """ 24 | struct ExceptionalResponse <: Exception 25 | response::HTTP.Response 26 | end 27 | 28 | Base.show(io::IO, ex::ExceptionalResponse) = print(io, "ExceptionalResponseException: $(ex.response.status) - $(Dict(ex.response.headers))") 29 | 30 | ### 31 | 32 | 33 | """ 34 | RuntimeException 35 | 36 | Represents an unexpected and unhandled runtime exceptions. An error event will be logged and the 37 | exception will be sent to the client, depending on the environment 38 | (the error stack is dumped by default in dev mode or an error message is displayed in production). 39 | 40 | It allows defining custom error message and info, as well as an error code, in addition to the exception object. 41 | 42 | # Arguments 43 | - `message::String` 44 | - `info::String` 45 | - `code::Int` 46 | - `ex::Union{Nothing,Exception}` 47 | """ 48 | struct RuntimeException <: Exception 49 | message::String 50 | info::String 51 | code::Int 52 | ex::Union{Nothing,Exception} 53 | end 54 | 55 | 56 | """ 57 | RuntimeException(message::String, code::Int) 58 | 59 | `RuntimeException` constructor using `message` and `code`. 60 | """ 61 | RuntimeException(message::String, code::Int) = RuntimeException(message, "", code, nothing) 62 | 63 | 64 | """ 65 | RuntimeException(message::String, info::String, code::Int) 66 | 67 | `RuntimeException` constructor using `message`, `info` and `code`. 68 | """ 69 | RuntimeException(message::String, info::String, code::Int) = RuntimeException(message, info, code, nothing) 70 | 71 | 72 | """ 73 | Base.show(io::IO, ex::RuntimeException) 74 | 75 | Custom printing of `RuntimeException` 76 | """ 77 | Base.show(io::IO, ex::RuntimeException) = print(io, "RuntimeException: $(ex.code) - $(ex.info) - $(ex.message)") 78 | 79 | ### 80 | 81 | """ 82 | struct InternalServerException <: Exception 83 | 84 | Dedicated exception type for server side exceptions. Results in a 500 error by default. 85 | 86 | # Arguments 87 | - `message::String` 88 | - `info::String` 89 | - `code::Int` 90 | """ 91 | struct InternalServerException <: Exception 92 | message::String 93 | info::String 94 | code::Int 95 | end 96 | 97 | 98 | """ 99 | InternalServerException(message::String) 100 | 101 | External `InternalServerException` constructor accepting a custome message. 102 | """ 103 | InternalServerException(message::String) = InternalServerException(message, "", 500) 104 | 105 | 106 | """ 107 | InternalServerException() 108 | 109 | External `InternalServerException` using default values. 110 | """ 111 | InternalServerException() = InternalServerException("Internal Server Error") 112 | 113 | ### 114 | 115 | 116 | """ 117 | struct NotFoundException <: Exception 118 | 119 | Specialized exception representing a not found resources. Results in a 404 response being sent to the client. 120 | 121 | # Arguments 122 | - `message::String` 123 | - `info::String` 124 | - `code::Int` 125 | - `resource::String` 126 | """ 127 | struct NotFoundException <: Exception 128 | message::String 129 | info::String 130 | code::Int 131 | resource::String 132 | end 133 | 134 | 135 | """ 136 | NotFoundException(resource::String) 137 | 138 | External constructor allowing to pass the name of the not found resource. 139 | """ 140 | NotFoundException(resource::String) = NotFoundException("$resource can not be found", "", 404, resource) 141 | 142 | 143 | """ 144 | NotFoundException() 145 | 146 | External constructor using default arguments. 147 | """ 148 | NotFoundException() = NotFoundException("") 149 | 150 | ### 151 | 152 | 153 | """ 154 | struct FileExistsException <: Exception 155 | 156 | Custom exception type for signaling that the requested file already exists. 157 | """ 158 | struct FileExistsException <: Exception 159 | path::String 160 | end 161 | 162 | 163 | """ 164 | Base.show(io::IO, ex::FileExistsException) 165 | 166 | Custom printing for `FileExistsException` 167 | """ 168 | Base.show(io::IO, ex::FileExistsException) = print(io, "FileExistsException: $(ex.path)") 169 | 170 | end --------------------------------------------------------------------------------