├── .formatter.exs ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── guides ├── interface-design-for-http-streaming.md ├── security-with-https.md └── writing-middleware-with-macros.md ├── lib ├── mix │ └── tasks │ │ └── raxx │ │ ├── kit.ex │ │ └── new.ex └── raxx │ └── kit.ex ├── mix.exs ├── mix.lock ├── priv └── template │ ├── Dockerfile.eex │ ├── README.md.eex │ ├── _DOTFILE.dockerignore.eex │ ├── _DOTFILE.formatter.exs │ ├── _DOTFILE.gitignore │ ├── _build │ └── _DOTFILE.dummy.eex │ ├── bin │ └── start.sh.eex │ ├── config │ └── config.exs.eex │ ├── deps │ └── _DOTFILE.dummy.eex │ ├── docker-compose.yml.eex │ ├── lib │ ├── app_name.ex.eex │ └── app_name │ │ ├── api.ex.eex │ │ ├── api │ │ ├── actions │ │ │ ├── not_found.ex.eex │ │ │ └── welcome_message.ex.eex │ │ └── router.ex.eex │ │ ├── application.ex.eex │ │ ├── repo.ex.eex │ │ ├── www.ex.eex │ │ └── www │ │ ├── _DOTFILE.gitignore.eex │ │ ├── actions │ │ ├── home_page.ex.eex │ │ ├── home_page.html.eex.eex │ │ ├── not_found_page.ex.eex │ │ └── not_found_page.html.eex.eex │ │ ├── assets │ │ ├── main.js.eex │ │ └── main.scss.eex │ │ ├── layout.ex.eex │ │ ├── layout.html.eex.eex │ │ ├── package-lock.json.eex │ │ ├── package.json.eex │ │ ├── page_header.html.eex.eex │ │ ├── public │ │ ├── favicon.ico │ │ ├── main.css.eex │ │ └── main.js.eex │ │ └── router.ex.eex │ ├── mix.exs.eex │ ├── mix.lock │ ├── priv │ ├── localhost │ │ ├── certificate.pem │ │ ├── certificate_key.pem │ │ └── certificate_signing_request.pem │ └── repo │ │ └── migrations │ │ └── _DOTFILE.gitignore.eex │ └── test │ ├── app_name │ ├── api │ │ └── actions │ │ │ └── welcome_message_test.exs.eex │ ├── database_test.exs.eex │ └── www │ │ └── actions │ │ └── home_page_test.exs.eex │ ├── app_name_test.exs.eex │ ├── support │ └── repo_case.ex.eex │ └── test_helper.exs.eex ├── scripts └── docker_test.sh └── test ├── raxx └── kit_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # The latest archive is part of the repository for startup 23 | !raxx_kit.ez 24 | 25 | # Ignore package tarball (built via "mix hex.build"). 26 | raxx_kit-*.tar 27 | 28 | # Where, by convention, test projects are created. 29 | /demo/ 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | sudo: required 3 | services: 4 | - docker 5 | elixir: 6 | - 1.7.3 7 | otp_release: 8 | - 20.0 9 | before_script: 10 | - "MIX_ENV=test mix local.hex --force && MIX_ENV=test mix deps.get" 11 | script: 12 | - MIX_ENV=test mix test 13 | - mix format --check-formatted 14 | - ./scripts/docker_test.sh 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [0.12.2](https://github.com/CrowdHailer/raxx_kit/tree/0.12.2) - 2020-04-07 8 | 9 | ### Changed 10 | 11 | - Update Elixir version in `Dockerfile.eex` to the newest posible `1.10.2` 12 | 13 | ## [0.12.1](https://github.com/CrowdHailer/raxx_kit/tree/0.12.1) - 2019-06-29 14 | 15 | ### Fixed 16 | 17 | - Mix task `raxx.new` updated to work with Elixir 1.9. 18 | 19 | ## [0.12.0](https://github.com/CrowdHailer/raxx_kit/tree/0.12.0) - 2019-05-02 20 | 21 | ### Added 22 | 23 | - Sessions with CSRF protection and flash messages. 24 | - Optional variables for templates. 25 | 26 | ## [0.11.0](https://github.com/CrowdHailer/raxx_kit/tree/0.11.0) - 2019-04-16 27 | 28 | ### Changed 29 | 30 | - Updated dependencies to use raxx `1.0`. 31 | 32 | ## [0.10.1](https://github.com/CrowdHailer/raxx_kit/tree/0.10.1) - 2019-03-15 33 | 34 | ### Fixed 35 | 36 | - `.dockerignore` is properly handled as a template. 37 | - Move `.gitignore` for node assets to `WWW` directory. 38 | 39 | ## [0.10.0](https://github.com/CrowdHailer/raxx_kit/tree/0.10.0) - 2019-02-17 40 | 41 | ### Added 42 | 43 | - JSON API projects can be generated by using the `--api` flag. 44 | 45 | ### Changed 46 | 47 | - Task `raxx.kit` renamed to `raxx.new`. 48 | 49 | ### Fixed 50 | 51 | - Add empty `package-lock.json` file to project so it is not generated by docker user. 52 | - Keep node modules in a named volume so they are not written back to host project directory. 53 | 54 | ## [0.9.2](https://github.com/CrowdHailer/raxx_kit/tree/0.9.2) - 2019-01-10 55 | 56 | ### Fixed 57 | 58 | - Using both `--docker` and `--ecto` now correctly creates the `docker-compose.yml` filed. 59 | 60 | ## [0.9.1](https://github.com/CrowdHailer/raxx_kit/tree/0.9.1) - 2019-01-07 61 | 62 | ### Added 63 | 64 | - View helpers and partials added to app layout module. 65 | 66 | ## [0.9.0](https://github.com/CrowdHailer/raxx_kit/tree/0.9.0) - 2019-01-06 67 | 68 | ### Changed 69 | 70 | - Use `Raxx.Stack` for runtime middleware. 71 | - Move assets and public files into `www` directory. 72 | - Clean up `child_spec` functions for each service. 73 | - Namespace all action modules under `Actions` module. 74 | - Create a separate `WWW.Router` module. 75 | - Add an example integration test using `:httpc` 76 | 77 | ## [0.8.4](https://github.com/CrowdHailer/raxx_kit/tree/0.8.4) - 2019-01-06 78 | 79 | ### Added 80 | 81 | - `mix raxx.new` added as an alias for `mix raxx.kit` task. 82 | 83 | ### Removed 84 | 85 | - The `--apib` switch did not produce a viable project and has been removed as an option. 86 | 87 | ## [0.8.3](https://github.com/CrowdHailer/raxx_kit/tree/0.8.3) - 2018-11-24 88 | 89 | ### Fixed 90 | 91 | - With `--ecto` option don't generate an empty migrations directory. 92 | 93 | ## [0.8.2](https://github.com/CrowdHailer/raxx_kit/tree/0.8.2) - 2018-11-24 94 | 95 | ### Fixed 96 | 97 | - Use `section`-based routes in the generated project to avoid warnings. 98 | 99 | ## [0.8.1](https://github.com/CrowdHailer/raxx_kit/tree/0.8.1) - 2018-11-24 100 | 101 | ### Fixed 102 | 103 | - Creating empty directories in the generated project. 104 | 105 | ## [0.8.0](https://github.com/CrowdHailer/raxx_kit/tree/0.8.0) - 2018-11-22 106 | 107 | ### Added 108 | 109 | - Ecto 3.0 boilerplate and postgres service when specifying `--ecto` flag. 110 | - Disabling code reloading with `--no-exsync` flag. 111 | - Code formatting run after the project is generated. 112 | 113 | ### Changed 114 | 115 | - Compilation artifacts no longer pollute the host machine when using `--docker`. 116 | - `--docker` option uses elixir:1.7.4 as base image. 117 | 118 | ## [0.7.1](https://github.com/CrowdHailer/raxx_kit/tree/0.7.1) - 2018-11-14 119 | 120 | ### Added 121 | 122 | - SASS compilation when specifying the `--node-assets` flag. 123 | - Newly generated projects are formatted on creation. 124 | 125 | ## [0.7.0](https://github.com/CrowdHailer/raxx_kit/tree/0.7.0) - 2018-10-29 126 | 127 | ### Changed 128 | 129 | - Use Ace `0.18.0`. 130 | 131 | ## [0.6.0](https://github.com/CrowdHailer/raxx_kit/tree/0.6.0) - 2018-09-12 132 | 133 | ### Changed 134 | 135 | - Use Ace `0.17.0`. 136 | 137 | ## [0.5.5](https://github.com/CrowdHailer/raxx_kit/tree/0.5.5) - 2018-09-04 138 | 139 | ### Added 140 | 141 | - Home page template demonstrates using variables in a template. 142 | - Setting JavaScript variables is done on the home page. 143 | 144 | ### Changed 145 | 146 | - `--docker` option uses latest (elixir:1.7.3) as base image. 147 | 148 | ## [0.5.4](https://github.com/CrowdHailer/raxx_kit/tree/0.5.4) - 2018-09-03 149 | 150 | ### Changed 151 | 152 | - Generated `HTMLView` module has been removed, instead `Raxx.Layout` is used 153 | 154 | ## [0.5.3](https://github.com/CrowdHailer/raxx_kit/tree/0.5.3) - 2018-06-04 155 | 156 | ### Added 157 | 158 | - Moduledoc added to mix task 159 | - Explicit link to favicon in template 160 | 161 | ## [0.5.2](https://github.com/CrowdHailer/raxx_kit/tree/0.5.2) - 2018-05-10 162 | 163 | ### Fixed 164 | 165 | - Default `secure_port` is now `8443`, and no longer the same as default `port` 166 | 167 | ## [0.5.1](https://github.com/CrowdHailer/raxx_kit/tree/0.5.1) - 2018-04-29 168 | 169 | ### Added 170 | 171 | - Environment configuration for ports. 172 | 173 | ## [0.5.0](https://github.com/CrowdHailer/raxx_kit/tree/0.5.0) - 2018-04-28 174 | 175 | ### Added 176 | 177 | - New `HTMLView` module handles templates and HTML escaping 178 | 179 | ## [0.4.4](https://github.com/CrowdHailer/raxx_kit/tree/0.4.4) - 2018-04-23 180 | 181 | ### Fixed 182 | 183 | - Recompilation is triggered for `.js` and `.css` files. 184 | 185 | ## [0.4.3](https://github.com/CrowdHailer/raxx_kit/tree/0.4.3) - 2018-04-22 186 | 187 | ### Fixed 188 | 189 | - dotfiles, such as `.gitignore` are prefixed in template to fix issues with priv dir and archives. 190 | 191 | ## [0.4.2](https://github.com/CrowdHailer/raxx_kit/tree/0.4.2) - 2018-04-21 192 | 193 | ### Added 194 | 195 | - Project information for release on hex.pm. 196 | 197 | ## [0.4.1](https://github.com/CrowdHailer/raxx_kit/tree/0.4.1) - 2018-04-21 198 | 199 | ### Added 200 | 201 | - Generated project includes a `.gitignore` file. 202 | 203 | ## [0.4.0](https://github.com/CrowdHailer/raxx_kit/tree/0.4.0) - 2018-03-22 204 | 205 | ### Added 206 | 207 | - Sensible Docker configuration created when using the `--docker` option. 208 | - Add supervised npm scripts to template project with `--node-assets` option. 209 | 210 | ## [0.3.0](https://github.com/CrowdHailer/raxx_kit/tree/0.3.0) - 2018-02-11 211 | 212 | ### Added 213 | 214 | - Unit test for HomePage action in project template. 215 | - API Blueprint router using the `--apib` option. 216 | - SSL certificates for an https endpoint, default port 8443. 217 | 218 | ## [0.2.0](https://github.com/CrowdHailer/raxx_kit/tree/0.2.0) - 2018-02-11 219 | 220 | Tokumei project is renamed Raxx Kit. 221 | This was to reflect a change in scope. 222 | The focus of Raxx Kit will be project generators for different usecases. 223 | Other features from the Tokumei framework have been migrated to appropriate Raxx middleware projects. 224 | 225 | ### Added 226 | 227 | - `raxx.kit` task, which accepts a `name` and optional `--module`. 228 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Precheck 2 | 3 | * For help and support, use [elixir-lang slack channel](https://elixir-lang.slack.com/messages/C56H3TBH8/) 4 | * For bugs, do a quick search and make sure the bug has not yet been reported 5 | * Ensure that this issue is related to the raxx_kit library and not one of the dependencies listed in mix.exs (Raxx, Ace, etc.) 6 | 7 | ### Environment 8 | 9 | * Elixir version (elixir -v): 10 | * Operating system: 11 | 12 | ### Expected behavior 13 | 14 | 15 | ### Actual behavior 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raxx.Kit 2 | 3 | **Get started with [Raxx](https://github.com/crowdhailer/raxx)/[Ace](https://github.com/CrowdHailer/Ace).** 4 | 5 | ```sh 6 | $ mix archive.install hex raxx_kit 7 | $ mix raxx.new my_app 8 | ``` 9 | 10 | ### Options 11 | 12 | - `--api`: Creates a JSON API project, instead of HTML pages. 13 | 14 | - `--ecto`: Adds Ecto as a dependency and configures project to use 15 | a Postgres database. If used with `--docker` flag, a docker-compose service 16 | with the database will get generated. 17 | 18 | - `--node-assets`: Add JavaScript compilation as part of a generated project. 19 | Works with or without docker. 20 | 21 | - `--docker`: Create `Dockerfile` and `docker-compose.yml` in template. 22 | This allows local development to be conducted completly in docker. 23 | 24 | - `--module`: Used to name the top level module used in the generated project. 25 | Without this option the module name will be generated from path option. 26 | 27 | ```sh 28 | $ mix raxx.new my_app 29 | 30 | # Is equivalent to 31 | $ mix raxx.new my_app --module MyApp 32 | ``` 33 | 34 | - `--no-exsync`: Doesn't include exsync in the generated project. Changed 35 | files won't be rebuilt on the fly when the app is running. 36 | 37 | ### Next 38 | 39 | - [Check Raxx documentation on hexdocs](https://hexdocs.pm/raxx) 40 | - [Join Raxx discussion on slack](https://elixir-lang.slack.com/messages/C56H3TBH8/) 41 | 42 | ### Features 43 | 44 | - Isolated web layer with [Raxx](https://github.com/crowdhailer/raxx) 45 | - HTTP/2 support with [Ace](https://github.com/CrowdHailer/Ace) server 46 | - Middleware for request logging and static content 47 | - Sessions and flash messages 48 | - Safe HTML templating 49 | - [Ecto 3.0](https://github.com/elixir-ecto/ecto_sql) and PostgreSQL integration 50 | - Controller unit tests 51 | - Code reloading with [ExSync](https://github.com/falood/exsync) 52 | 53 | [Tutorial for building a distributed chatroom with Raxx.Kit](http://crowdhailer.me/2018-05-01/building-a-distributed-chatroom-with-raxx-kit/) 54 | -------------------------------------------------------------------------------- /guides/interface-design-for-http-streaming.md: -------------------------------------------------------------------------------- 1 | # Interface design for HTTP streaming 2 | 3 | - *[CrowdHailer](http://crowdhailer.me/) - 25 September 2017* 4 | 5 | --- 6 | 7 | Raxx is a server interface originally based on Ruby's [Rack interface](https://rack.github.io/). 8 | To support streaming, Raxx has fundamentally changed from version 0.12.0. 9 | These changes were necessary to support HTTP/2 in [Ace](https://hex.pm/packages/ace). 10 | 11 | *If starting with Raxx after `0.12.0`, 12 | you can find the latest documentation [here](https://hexdocs.pm/raxx/).* 13 | 14 | ### HTTP overview 15 | 16 | The purpose of an HTTP server is to transform a client request into a response. 17 | The simplest representation would be a single function accepting a request and returning a response. 18 | 19 | Prior to `0.12.0` the Raxx interface was built on this simple concept. 20 | 21 | I have previously [talked](https://www.youtube.com/watch?v=80AXtvXFIA4) 22 | and [written](https://hexdocs.pm/tokumei/why-raxx.html) 23 | about this implementation. 24 | 25 | This simple implementation has been deployed successfully. 26 | However the absence of a streaming solution is limiting for several usecases. 27 | 28 | - Inefficient to hold complete messages in memory when the body is large. 29 | - Impossible to send a response after just reading the head of a request. 30 | - Unable to implement server streaming when data is sent indefinitely as it becomes available. 31 | - Limiting when working with HTTP/2 features such as push promises. 32 | 33 | ### HTTP streaming 34 | 35 | Streaming is when part of a message is acted upon without knowing the rest. 36 | An HTTP message (request or response) consists of the following parts: 37 | 38 | - Message head: A start line with mandatory metadata about the message, 39 | i.e path of a request or status of a response; 40 | plus additional metadata in the form of headers, such as `content-type`. 41 | - Message fragment: A part of the message body, 42 | there may be none up to an unlimited number of these fragments. 43 | - Message tail: The end of the message, 44 | which may include optional metadata in the form of trailers. 45 | 46 | An HTTP streaming server is a long running process, 47 | which can process HTTP message parts, as well as erlang messages from the application. 48 | 49 | The `Raxx.Server` is a behaviour to define such a server. 50 | An implementation of this behaviour instructs a process how to interact with a client. 51 | 52 | ## The Raxx Server 53 | 54 | A Raxx server module needs to implement 4 callbacks. 55 | There are 3 callbacks to handle HTTP parts from the client. 56 | The final callback is for handling messages issued from other application processes. 57 | 58 | - `handle_headers/2` 59 | - `handle_fragment/2` 60 | - `handle_trailer/2` 61 | - `handle_info/2` 62 | 63 | Acceptable return types are the same for every callback in this behaviour. 64 | Returned can be a tuple consisting of message parts to the client and the servers new state or a complete response. 65 | That is the end of the servers interaction with a client. 66 | 67 | i.e. 68 | 69 | To send some more data: 70 | ```elixir 71 | def handle_fragment(fragment, state) do 72 | # ... processing 73 | {[Raxx.fragment("Some data")], new_state} 74 | end 75 | ``` 76 | 77 | To not send any data but keep running: 78 | ```elixir 79 | def handle_fragment(fragment, state) do 80 | # ... processing 81 | {[], new_state} 82 | end 83 | ``` 84 | 85 | Once a complete response is sent the server process will be terminated. 86 | Therefore a full response can be sent without setting a new state. 87 | ```elixir 88 | def handle_fragment(fragment, state) do 89 | # ... processing 90 | Raxx.response(:no_content) 91 | end 92 | ``` 93 | 94 | ### Simplicity and purity 95 | 96 | In the original implementation of Raxx the callback implementation could be pure functions. 97 | I asserted that [using pure functions made application code simpler](file:///home/peter/Projects/Tokumei/app/doc/why-raxx.html#purity), and easier to test. 98 | 99 | This update to Raxx keeps callbacks pure. 100 | 101 | *This is exactly the pattern of a `GenServer`, 102 | where all side effects, such as replying to a call, can be represented in the return values.* 103 | 104 | *My concern with the plug interface has always been that certain things can only be achieved by directly causing side effects from within application code. 105 | It is my opinion that this leads to much of the complexity of the `Plug.Conn` object,* 106 | 107 | ## Examples 108 | 109 | ### Client streaming data 110 | 111 | Naive server to save upload files to an assets directory. 112 | 113 | ```elixir 114 | defmodule FileUpload do 115 | use Raxx.Server 116 | 117 | def handle_headers(%{method: :PUT, body: true, path: ["assets", name]}, _config) do 118 | {:ok, io_device} = File.open("assets/#{name}") 119 | {[], {:file, device}} 120 | end 121 | 122 | def handle_fragment(fragment, state = {:file, device}) do 123 | IO.write(device, fragment) 124 | {[], state} 125 | end 126 | 127 | def handle_trailers(_trailers, state) do 128 | Raxx.response(:see_other) 129 | |> Raxx.set_header("location", "/") 130 | end 131 | end 132 | ``` 133 | 134 | ### Server streaming response 135 | 136 | This server will join a chatroom upon receiving a client. 137 | It will then stream data to that client as messages are published to the chatroom. 138 | 139 | ```elixir 140 | defmodule SubscribeToMessages do 141 | use Raxx.Server 142 | 143 | def handle_headers(_request, _config) do 144 | {:ok, _} = ChatRoom.join() 145 | Raxx.response(:ok) 146 | |> Raxx.set_header("content-type", "text/plain") 147 | |> Raxx.set_body(true) 148 | end 149 | 150 | def handle_info({:publish, data}, config) do 151 | {[Raxx.fragment(data)], config} 152 | end 153 | end 154 | ``` 155 | 156 | ## Canonical message 157 | 158 | A stream of parts belongs to a single request or response. 159 | The body of a message is considered the same body regardless of the fragments it is separated into. 160 | 161 | The simple request response model used in Rack (and previously Raxx) is just a special case 162 | where each stream has one part. 163 | 164 | To make it easer to work with HTTP messages Raxx supports a canonical view for complete HTTP messages. 165 | 166 | The body of a request (or response) can be a boolean, or the full body as a binary. 167 | This allows a single request to be represented as a single object or a list of parts. 168 | 169 | In this example these two representations are of the same request. 170 | ```elixir 171 | streamed_request = [ 172 | %Raxx.Response{status: 200, body: true}, 173 | %Raxx.Fragment{data: "Hello, "}, 174 | %Raxx.Fragment{data: "World!"}, 175 | %Raxx.Trailer{headers: []} 176 | ] 177 | 178 | complete_request = %Raxx.Response{status: 200, headers: [], body: "Hello, World!"} 179 | ``` 180 | 181 | Note in a `Raxx.Server` `handle_headers/2` is always called as soon as the request head has been read. 182 | Therefore the request passed to this callback will always have a boolean value for the body. 183 | 184 | A server can collapse the parts of a request into its cannonical version. 185 | This could be done before executing any business logic in some cases. 186 | This might be the simplest solution for a JSON API where neither request or response is ever very large. 187 | 188 | This allows simple behaviour to have a simple implementation, without making working with streams harder. 189 | 190 | A simple server where all the requests are collapsed before being handled could look like the following: 191 | 192 | ```elixir 193 | defmodule Raxx.SimpleServer do 194 | use Raxx.SimpleServer 195 | 196 | # Not a Raxx.Server callback 197 | def handle_request(request, config) do 198 | # Work with request and config 199 | Raxx.response(:ok) 200 | end 201 | 202 | # When no body already a complete request 203 | def handle_headers(request = %{body: false}, config) do 204 | handle_request(request, config) 205 | end 206 | 207 | # Body expected start an empty buffer to collect data 208 | def handle_headers(request = %{body: true}, config) do 209 | buffer = "" 210 | {[], {request, buffer, config}} 211 | end 212 | 213 | def handle_fragment(data, {request, buffer, config}) do 214 | {[], {request, buffer <> data, config}} 215 | end 216 | 217 | # Always called for a request that has a body 218 | def handle_trailers(trailers, {request = %{headers: headers}, body, config}) do 219 | complete_headers = headers ++ trailers 220 | request = %{request | headers: complete_headers, body: body} 221 | handle_request(request, config) 222 | end 223 | end 224 | ``` 225 | -------------------------------------------------------------------------------- /guides/security-with-https.md: -------------------------------------------------------------------------------- 1 | # Security with HTTPS 2 | 3 | *If familiar with generating certificates just to [server setup](#server-setup)* 4 | 5 | ## What is TLS (SSL) 6 | 7 | - **TLS:** Transport Layer Security 8 | - **SSL:** Secure Sockets Layer 9 | 10 | Both protocols provide encryption and authentication to secure communication between a client and server. 11 | 12 | TLS is the successor to SSL, however the terms are often used interchangable. 13 | The last version of SSL (3.0) has demonstrated vulnerabilities and should not be used. 14 | 15 | #### "By Port" or "By Protocol" 16 | 17 | A secure connection can be estabilshed in two distinct ways: 18 | 19 | - **By Port:** Connect to a specific port that serves secure connections, e.g. 443 for https (secure web). 20 | - **By Protocol:** First connect to an insecure port, second negotiate an upgrade. 21 | 22 | *As far as I am aware either method of initiation is equally valid. 23 | If anyone knows more on best practices please open a pull request.* 24 | 25 | #### Theres's more 26 | 27 | "By Port" is commonly referred to as "SSL" event when it is an explicit connection to a TLS endpoint. "By Protocol" is commonly referred to as "TLS" even when it is an implicit upgrade to a SSL connection. A comprehensive overview is available at [SSL versus TLS – What’s the difference?](https://luxsci.com/blog/ssl-versus-tls-whats-the-difference.html). 28 | 29 | ## Generating credentials 30 | 31 | To serve encrypted connections a server requires: 32 | 33 | - A certificate for the domain: `mydomain/certificate.pem` 34 | - The private key associated with the certificate: `mydomain/certificate_key.pem` 35 | 36 | #### Generating a Key 37 | 38 | A new key can be created using `openssl`, which is already present on most linux machines. 39 | 40 | ``` 41 | $ openssl genrsa -out certificate_key.pem 42 | ``` 43 | 44 | #### Getting a signed Certificate 45 | 46 | Producing a certificate signing request (CSR) is the first step to getting a certificate. 47 | This is also done with `openssl` and you will be guided to provide details about your certificate. 48 | 49 | ``` 50 | $ openssl req -new -key certificate_key.pem -out certificate_signing_request.pem 51 | You are about to be asked to enter information that will be incorporated 52 | into your certificate request. 53 | What you are about to enter is what is called a Distinguished Name or a DN. 54 | There are quite a few fields but you can leave some blank 55 | For some fields there will be a default value, 56 | If you enter '.', the field will be left blank. 57 | ----- 58 | Country Name (2 letter code) [AU]:UK 59 | State or Province Name (full name) [Some-State]:London 60 | Locality Name (eg, city) []:London 61 | Organization Name (eg, company) [Internet Widgits Pty Ltd]:Workshop 14 Limited 62 | Organizational Unit Name (eg, section) []: 63 | Common Name (e.g. server FQDN or YOUR name) []:example.com 64 | Email Address []: 65 | 66 | Please enter the following 'extra' attributes 67 | to be sent with your certificate request 68 | A challenge password []: 69 | An optional company name []: 70 | ``` 71 | 72 | For a client to authenticate a server the CSR must be signed by a certificate authority (CA) it recognises. 73 | In development we can skip the CA by signing the certificate ourselves. 74 | 75 | ``` 76 | $ openssl x509 -req -days 365 \ 77 | -in certificate_signing_request.pem \ 78 | -signkey certificate_key.pem \ 79 | -out certificate.pem 80 | ``` 81 | 82 | ## Server setup 83 | 84 | ### Ace 85 | 86 | To start a secure server with Ace use `Ace.HTTPS` in place of `Ace.HTTP`. 87 | Both modules provide the same interface with a few extra options required to start HTTPS. 88 | 89 | ```elixir 90 | certificate_path = Application.app_dir(:my_app, "priv/example.com/certificate.pem") 91 | certificate_key_path = Application.app_dir(:my_app, "priv/example.com/certificate_key.pem") 92 | 93 | Ace.HTTP2.Service.start_link({MyApp, :noconfig}, [ 94 | certfile: certificate_path, 95 | keyfile: certificate_key_path, 96 | port: 8443 97 | ]) 98 | ``` 99 | 100 | The key files must be in a projects `priv` directory if using releases. 101 | This is a convention inherited from erlang. 102 | The process of generating releases assumes this convention is followed 103 | 104 | TODO link to https example in WaterCooler. 105 | 106 | ## Further topics 107 | 108 | - Security headers to ensure a user uses HTTPS, see [Plug.SSL](https://github.com/elixir-lang/plug/blob/master/lib/plug/ssl.ex) 109 | - Automatically creating a certificate using the Acme (Automatic Certificate Management Environment) protocol and lets encryp. 110 | - What is a cacerts file? http://www.phoenixframework.org/docs/configuration-for-ssl 111 | -------------------------------------------------------------------------------- /guides/writing-middleware-with-macros.md: -------------------------------------------------------------------------------- 1 | # Writing middleware with Macros 2 | 3 | ### RAXX 0.12.0 MAKES CHANGES TO INTERFACE DESCRIBED HERE. 4 | 5 | - *[CrowdHailer](http://crowdhailer.me/) - 08 April 2017* 6 | 7 | --- 8 | 9 | Middleware is software that modifies the behaviour of other software. 10 | They are the perfect place to implement functionality that applies to many routes. 11 | 12 | Examples of such cross cutting functionality include authorization, parsing content and monitoring. 13 | In this walkthrough we will create middleware that adds logging to an application. 14 | 15 | ## Hello, World! 16 | 17 | Let's get started with an application that needs logging. 18 | We will use a simple hello world example. 19 | It has one route for the home page. 20 | All other requests return with 404. 21 | 22 | ```elixir 23 | defmodule GreetingApp do 24 | alias Raxx.Response 25 | 26 | def handle_request(%{path: []}, _config) do 27 | Response.ok("Hello, World!") 28 | end 29 | 30 | def handle_request(_request, _config) do 31 | Response.not_found("Nothing here.") 32 | end 33 | end 34 | ``` 35 | 36 | ## Defining Middleware 37 | 38 | Our greeting app now just requires logging. 39 | The naive solution is to add call to `Logger` in every action handler. 40 | 41 | There are a few reasons why this solution is poor. 42 | First we will be duplicating code in every action handler, which in turn means that it is easy to make a mistake so that one routes ends up without the logging required. 43 | Second the added code is bundled into that action handler and obscures the main purpose of that route. 44 | 45 | A desirable solution is one that adds logging to every route without modifying the action handler's code. 46 | We can build this solution using `defoverridable/1`. 47 | This allows us to redefine a function. From this redefined function we can call back to the previous implementation using `super`. 48 | 49 | In combination this allows us to add logging to our app without modifying or obscuring existing action handlers. 50 | 51 | ```elixir 52 | defmodule GreetingApp do 53 | 54 | # ... original action handlers the same 55 | 56 | defoverridable [handle_request: 2] 57 | 58 | require Logger 59 | 60 | def handle_request(request, config) do 61 | 62 | # Call the previous version of `handle_request/2` using `super` 63 | response = super(request, config) 64 | log_request_and_response(request, response) 65 | response 66 | end 67 | 68 | defp log_request_and_response(%{path: path, method: method}, %{status: status}) do 69 | path = Enum.join(path, "/") 70 | Logger.info("#{method} #{path} -> #{status}") 71 | end 72 | end 73 | ``` 74 | 75 | We can now start `GreetingApp` app. 76 | After a few requests have been made to the server we can see the logs include extra details for requests. 77 | 78 | ``` 79 | [info] GreetingApp is listening on port: 8080 80 | [info] GET / -> 200 81 | [info] GET /random -> 404 82 | ``` 83 | 84 | ## Reusing Middleware 85 | 86 | Great. This logger does exactly what we need. 87 | 88 | Let's extract our logging functionality so it can be reused across all our projects. 89 | To implement this version we need to turn to Elixir macros. 90 | A Macro is just code that writes code. 91 | We want to write a `MyLogger` module that adds logging to any module it is used in. 92 | 93 | This next example does just that. 94 | 95 | ```elixir 96 | defmodule MyLogger do 97 | 98 | # Define a macro that will be called when this module is used. 99 | defmacro __using__(_opts) do 100 | quote do 101 | 102 | # The module will require the logger later 103 | require Logger 104 | 105 | # We can't override functions that do not exist. 106 | # Adding this line allows to module override the function after the user has defined all the routes. 107 | @before_compile unquote(__MODULE__) 108 | end 109 | end 110 | 111 | # Define a macro that will be called after all module definitions 112 | defmacro __before_compile__(_env) do 113 | quote do 114 | 115 | # Define all the code that we want to add to our module 116 | defoverridable [handle_request: 2] 117 | 118 | def handle_request(request, config) do 119 | 120 | # Call the previous version of `handle_request/2` using `super` 121 | response = super(request, config) 122 | log_request_and_response(request, response) 123 | response 124 | end 125 | 126 | defp log_request_and_response(%{path: path, method: method}, %{status: status}) do 127 | path = Enum.join(path, "/") 128 | Logger.info("#{method} #{path} -> #{status}") 129 | end 130 | end 131 | end 132 | end 133 | ``` 134 | 135 | ## Combining middleware 136 | 137 | Any Raxx application can now have logging by using `MyLogger`. 138 | It is possible to override a function multiple times. 139 | In this way multiple middleware can be defined and stacked to combine their behaviour. 140 | 141 | All functionality in Tokumei is contained in middleware, including routing. 142 | 143 | To finish this example let's simplify our code. 144 | We can stack `MyLogger` with routing middleware included as part of Tokumei. 145 | 146 | ```elixir 147 | defmodule GreetingApp do 148 | alias Raxx.Response 149 | 150 | # middleware stack 151 | use Tokumei.NotFound 152 | use Tokumei.Routing 153 | use MyLogger 154 | 155 | route [] do 156 | Response.ok("Hello, World!") 157 | end 158 | end 159 | ``` 160 | 161 | ## Conclusion 162 | 163 | Middleware allows us to separate different concerns of our application into easily reusable chunks. 164 | It is simple to implement using the constructs provided by Elixir. 165 | -------------------------------------------------------------------------------- /lib/mix/tasks/raxx/kit.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Raxx.Kit do 2 | use Mix.Task 3 | 4 | @impl Mix.Task 5 | def run(_arguments) do 6 | Mix.shell().error("Task `raxx.kit` has been deprecated; use `raxx.new`.") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/mix/tasks/raxx/new.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Raxx.New do 2 | use Mix.Task 3 | 4 | require Mix.Generator 5 | 6 | @shortdoc "Creates a new Raxx project for browsers" 7 | @switches [ 8 | api: :boolean, 9 | docker: :boolean, 10 | module: :string, 11 | node_assets: :boolean, 12 | no_exsync: :boolean, 13 | ecto: :boolean 14 | ] 15 | 16 | @moduledoc """ 17 | Creates a new Raxx project for browsers. 18 | 19 | It expects the name of the project as the argument. 20 | 21 | mix raxx.new NAME [--ecto] [--node-assets] [--docker] [--module ModuleName] 22 | [--no-exsync] 23 | 24 | ## Options 25 | 26 | - `--api`: Creates a JSON API project, instead of HTML pages. 27 | 28 | - `--ecto`: Adds Ecto as a dependency and configures project to use 29 | a Postgres database. If used with `--docker` flag, a docker-compose service 30 | with the database will get generated. 31 | 32 | - `--node-assets`: Add JavaScript compilation as part of a generated project. 33 | Works with or without docker. 34 | 35 | - `--docker`: Create `Dockerfile` and `docker-compose.yml` in template. 36 | This allows local development to be conducted completly in docker. 37 | 38 | - `--module`: Used to name the top level module used in the generated project. 39 | Without this option the module name will be generated from path option. 40 | 41 | ```sh 42 | $ mix raxx.new my_app 43 | 44 | # Is equivalent to 45 | $ mix raxx.new my_app --module MyApp 46 | ``` 47 | 48 | - `--no-exsync`: Doesn't include exsync in the generated project. Changed 49 | files won't be rebuilt on the fly when the app is running. 50 | 51 | """ 52 | 53 | @impl Mix.Task 54 | def run([]) do 55 | Mix.Tasks.Help.run(["raxx.new"]) 56 | end 57 | 58 | def run(options) do 59 | case OptionParser.parse!(options, strict: @switches) do 60 | {_, []} -> 61 | Mix.raise("raxx.new must be given a name `mix raxx.new `") 62 | 63 | {switches, [name]} -> 64 | {:ok, message} = Raxx.Kit.generate([{:name, name} | switches]) 65 | 66 | Mix.shell().info(message) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/raxx/kit.ex: -------------------------------------------------------------------------------- 1 | defmodule Raxx.Kit do 2 | @enforce_keys [ 3 | :name, 4 | :module, 5 | :api, 6 | :docker, 7 | :node_assets, 8 | :exsync, 9 | :ecto 10 | ] 11 | 12 | defstruct @enforce_keys 13 | 14 | def generate(options) do 15 | # TODO check if dir exists 16 | config = check_config!(options) 17 | 18 | # This calls File.mkdir_p!/1 and may raise a `File.Error` exception 19 | Mix.Generator.create_directory(config.name) 20 | 21 | File.cd!(config.name, fn -> 22 | assigns = Map.from_struct(config) 23 | 24 | filter_path = "/app_name/" <> if config.api, do: "www", else: "api" 25 | 26 | :ok = 27 | template_dir() 28 | |> Path.join("./**/*") 29 | |> Path.wildcard() 30 | |> Enum.reject(fn path -> path =~ filter_path end) 31 | |> Enum.each(©_template(&1, template_dir(), assigns)) 32 | 33 | if config.docker do 34 | # Getting npm and mix dependencies is handled in start.sh/Dockerfile 35 | Mix.shell().cmd("docker-compose run #{config.name} mix format") 36 | else 37 | # If using Docker mix/node/npm might not be available on host machine 38 | Mix.shell().cmd("mix deps.get") 39 | Mix.shell().cmd("mix format") 40 | 41 | if config.node_assets do 42 | File.cd!("lib/" <> config.name <> "/www", fn -> 43 | Mix.shell().cmd("npm install") 44 | end) 45 | end 46 | end 47 | end) 48 | 49 | run_instructions = 50 | case {config.docker, !!config.ecto} do 51 | {true, _} -> 52 | " docker-compose up" 53 | 54 | {false, false} -> 55 | " iex -S mix" 56 | 57 | {false, true} -> 58 | # the backslash at the end is not a typo, it's removing 59 | # the newline from the end of the heredoc 60 | """ 61 | nano config/config.exs # configure the #{config.module}.Repo database 62 | mix ecto.create 63 | mix ecto.migrate 64 | iex -S mix\ 65 | """ 66 | end 67 | 68 | message = 69 | """ 70 | Your Raxx project was created successfully. 71 | 72 | Get started: 73 | 74 | cd #{config.name} 75 | #{run_instructions} 76 | 77 | View on http://localhost:8080 78 | View on https://localhost:8443 (NOTE: uses a self signed certificate) 79 | """ 80 | |> String.trim_trailing() 81 | 82 | {:ok, message} 83 | end 84 | 85 | defp check_config!(options) do 86 | {:ok, name} = Keyword.fetch(options, :name) 87 | module = Keyword.get(options, :module, Macro.camelize(name)) 88 | docker = Keyword.get(options, :docker, false) 89 | api = Keyword.get(options, :api, false) 90 | node_assets = Keyword.get(options, :node_assets, false) 91 | exsync = !Keyword.get(options, :no_exsync, false) 92 | 93 | ecto = 94 | if Keyword.get(options, :ecto, false) do 95 | %{ 96 | db_name: name, 97 | db_username: generate_random(8), 98 | db_password: generate_random(18) 99 | } 100 | else 101 | false 102 | end 103 | 104 | %__MODULE__{ 105 | name: name, 106 | module: module, 107 | api: api, 108 | docker: docker, 109 | node_assets: node_assets, 110 | exsync: exsync, 111 | ecto: ecto 112 | } 113 | end 114 | 115 | defp template_dir() do 116 | Application.app_dir(:raxx_kit, "priv/template") 117 | end 118 | 119 | defp copy_template(file, root, assigns) do 120 | case File.read(file) do 121 | {:error, :eisdir} -> 122 | path = 123 | Path.relative_to(file, root) 124 | |> translate_path(assigns) 125 | 126 | Mix.Generator.create_directory(path) 127 | 128 | {:ok, template} -> 129 | original_path = Path.relative_to(file, root) 130 | 131 | {path, contents} = 132 | case String.split(original_path, ~r/\.eex$/) do 133 | [path, ""] -> 134 | path = translate_path(path, assigns) 135 | 136 | contents = 137 | try do 138 | EEx.eval_string(template, assigns: assigns) 139 | rescue 140 | e in EEx.SyntaxError -> 141 | Mix.shell().error( 142 | "Generator could not evaluate template under path '#{original_path}'" 143 | ) 144 | 145 | reraise e, __STACKTRACE__ 146 | end 147 | 148 | {path, contents} 149 | 150 | [path] -> 151 | path = translate_path(path, assigns) 152 | contents = template 153 | {path, contents} 154 | end 155 | 156 | if String.trim(contents) == "" do 157 | :ok 158 | else 159 | Mix.Generator.create_file(path, contents) 160 | end 161 | end 162 | end 163 | 164 | defp translate_path(path, assigns) do 165 | path 166 | |> String.replace("app_name", assigns.name, global: true) 167 | |> String.replace("_DOTFILE", "") 168 | end 169 | 170 | def generate_random(length) do 171 | :crypto.strong_rand_bytes(length) 172 | |> Base.encode64() 173 | |> binary_part(0, length) 174 | |> String.replace(["+", "/"], "_") 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RaxxKit.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :raxx_kit, 7 | version: "0.12.2", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | docs: [extras: ["README.md"], main: "readme"], 13 | package: package() 14 | ] 15 | end 16 | 17 | def application do 18 | [ 19 | extra_applications: [:logger] 20 | ] 21 | end 22 | 23 | defp deps do 24 | [ 25 | {:ex_doc, ">= 0.0.0", only: :dev} 26 | ] 27 | end 28 | 29 | defp description do 30 | """ 31 | Micro framework for web applications with Raxx and Ace. 32 | """ 33 | end 34 | 35 | defp package do 36 | [ 37 | maintainers: ["Peter Saxton"], 38 | licenses: ["Apache 2.0"], 39 | links: %{"GitHub" => "https://github.com/crowdhailer/raxx_kit"} 40 | ] 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, 3 | "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 7 | } 8 | -------------------------------------------------------------------------------- /priv/template/Dockerfile.eex: -------------------------------------------------------------------------------- 1 | <%= if @docker do %>FROM elixir:1.10.2 2 | 3 | # NOTE the WORKDIR should not be the users home dir as the will copy container cookie into host machine 4 | WORKDIR /opt/app 5 | 6 | <%= if @node_assets do %>RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - 7 | <% end %># Add tools needed for development 8 | # inotify-tools: gives filesystem events that are used to trigger recompilation 9 | RUN apt-get update && apt-get install -y inotify-tools<%= if @node_assets do %> nodejs<% end %> 10 | 11 | ENV MIX_ARTIFACTS_DIRECTORY=../mix_artifacts 12 | 13 | RUN mix local.hex --force && mix local.rebar --force && \ 14 | mkdir -p ${MIX_ARTIFACTS_DIRECTORY}/deps && mkdir -p ${MIX_ARTIFACTS_DIRECTORY}/_build && \ 15 | mkdir config 16 | 17 | # Build all dependencies separately to the application code 18 | COPY mix.* ./ 19 | RUN mix do deps.get 20 | COPY config/* config/ 21 | RUN mix deps.compile && MIX_ENV=test mix deps.compile 22 | 23 | # Add application code as final layer 24 | # This will skip the _build and deps directories as per .dockerignore 25 | COPY . . 26 | 27 | # Make sure the Docker image can be started without waiting for the project to compile 28 | # NOTE: mix deps.get is needed to update the freshly copied mix.lock file 29 | RUN mix do deps.get, compile 30 | 31 | CMD ["sh", "./bin/start.sh"] 32 | <% end %> 33 | -------------------------------------------------------------------------------- /priv/template/README.md.eex: -------------------------------------------------------------------------------- 1 | # <%= @module %> 2 | <%= if @docker do %> 3 | - Start your service with `docker-compose up` 4 | - Run project test suite with `docker-compose run <%= @name %> mix test` 5 | - Start IEx session in running service 6 | # Find a container id using docker ps 7 | docker exec -it bash 8 | 9 | # In container 10 | iex --sname debug --remsh app@$(hostname) 11 | 12 | Alternatively, you can still run the project directly, without docker: 13 | 14 | <%= if @ecto do %>- Start just the database service with `docker-compose up db` 15 | <% end %>- Install dependencies with `mix deps.get`<%= if @ecto do %> 16 | - prepare the database using `mix do ecto.create, ecto.migrate` 17 | <% end %>- Start your service with `iex -S mix` 18 | <% else %> 19 | - Install dependencies with `mix deps.get`<%= if @ecto do %> 20 | - make sure your postgres db is running and configured correctly in `config/config.exs` 21 | - prepare the database using `mix do ecto.create, ecto.migrate` 22 | <% end %>- Start your service with `iex -S mix` 23 | - Run project test suite with `mix test` 24 | <% end %> 25 | ## Learn more 26 | 27 | - Raxx documentation: https://hexdocs.pm/raxx 28 | - Slack channel: https://elixir-lang.slack.com/messages/C56H3TBH8/ 29 | -------------------------------------------------------------------------------- /priv/template/_DOTFILE.dockerignore.eex: -------------------------------------------------------------------------------- 1 | <%= if @docker do %># container builds its own artifacts 2 | _build 3 | deps 4 | 5 | # SEE docker-compose.yml for details 6 | container_mix_artifacts 7 | <% end %> 8 | -------------------------------------------------------------------------------- /priv/template/_DOTFILE.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /priv/template/_DOTFILE.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # If the VM crashes, it generates a dump, let's ignore it too. 14 | erl_crash.dump 15 | 16 | # Also ignore archive artifacts (built via "mix archive.build"). 17 | *.ez 18 | -------------------------------------------------------------------------------- /priv/template/_build/_DOTFILE.dummy.eex: -------------------------------------------------------------------------------- 1 | <%= "" %> 2 | -------------------------------------------------------------------------------- /priv/template/bin/start.sh.eex: -------------------------------------------------------------------------------- 1 | <%= if @docker do %>#!/usr/bin/env sh 2 | set -eu 3 | 4 | mix deps.get 5 | <%= if @node_assets do %> 6 | (cd lib/<%= @name %>/www ; npm install)<% end %> 7 | <%= if @ecto do %> 8 | # NOTE there is a race condition between the db service starting 9 | # and the project itself starting. This code will retry until the db can be connected to. 10 | attempts=20 11 | for i in `seq $attempts`; do 12 | mix ecto.create >/dev/null && break 13 | echo "Attempt $i / $attempts - failed to create db for the Repo" 14 | sleep 1 15 | done 16 | 17 | if [ $i -eq $attempts ]; then 18 | echo "Could not connect to the database, exiting" 19 | exit 2; 20 | fi 21 | 22 | mix ecto.migrate 23 | <% end %> 24 | elixir --sname app -S mix run --no-halt 25 | <% end %> 26 | -------------------------------------------------------------------------------- /priv/template/config/config.exs.eex: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | <%= if @ecto do %> 4 | config :<%= @name %>, 5 | ecto_repos: [<%= @module %>.Repo] 6 | 7 | config :<%= @name %>, <%= @module %>.Repo, 8 | # it can be overridden using the DATABASE_URL environment variable 9 | url: "ecto://<%= @ecto.db_username %>:<%= @ecto.db_password %>@localhost:6543/<%= @ecto.db_name %>?ssl=false&pool_size=10" 10 | 11 | if Mix.env() == :test do 12 | config :<%= @name %>, <%= @module %>.Repo, 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | end 15 | <% end %> 16 | <%= if @exsync do %> 17 | if Mix.env() == :dev do 18 | config :exsync, 19 | extra_extensions: [".js", ".css"] 20 | end 21 | <% end %> 22 | -------------------------------------------------------------------------------- /priv/template/deps/_DOTFILE.dummy.eex: -------------------------------------------------------------------------------- 1 | <%= "" %> 2 | -------------------------------------------------------------------------------- /priv/template/docker-compose.yml.eex: -------------------------------------------------------------------------------- 1 | <%= if @docker do %>version: '2' 2 | 3 | <%= if @node_assets do %>volumes: 4 | node_modules: 5 | <% end %>services: 6 | <%= @name %>: 7 | build: 8 | context: "." 9 | dockerfile: "Dockerfile" 10 | <%= if @ecto do %>depends_on: 11 | - db 12 | environment: 13 | - "DATABASE_URL=ecto://<%= @ecto.db_username %>:<%= @ecto.db_password %>@db:5432/<%= @ecto.db_name %>?ssl=false&pool_size=10" 14 | <% end %>ports: 15 | - 8080:8080 16 | - 8443:8443 17 | volumes: 18 | - .:/opt/app 19 | <%= if @node_assets do %>- node_modules:/opt/app/lib/<%= @name %>/www/node_modules 20 | <% end %>## uncomment the below lines if you want to see the contents of the 21 | ## container's deps/ and _build/ directories in your local project, 22 | ## under container_mix 23 | # - ./container_mix_artifacts:/opt/mix_artifacts 24 | <%= if @ecto do %> 25 | db: 26 | image: "postgres:9.6.11" 27 | environment: 28 | - POSTGRES_USER=<%= @ecto.db_username %> 29 | - POSTGRES_PASSWORD=<%= @ecto.db_password %> 30 | ports: 31 | - 6543:5432<% end %> 32 | <% end %> 33 | -------------------------------------------------------------------------------- /priv/template/lib/app_name.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %> do 2 | def welcome_message(name, greeting \\ "Hello") 3 | 4 | def welcome_message(nil, _greeting) do 5 | nil 6 | end 7 | 8 | def welcome_message(name, greeting) do 9 | "#{greeting}, #{name}!" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/api.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.API do 2 | 3 | def child_spec([server_options]) do 4 | {:ok, port} = Keyword.fetch(server_options, :port) 5 | %{ 6 | id: {__MODULE__, port}, 7 | start: {__MODULE__, :start_link, [server_options]}, 8 | type: :supervisor 9 | } 10 | end 11 | 12 | def init() do 13 | %{} 14 | end 15 | 16 | def start_link(server_options) do 17 | start_link(init(), server_options) 18 | end 19 | 20 | def start_link(config, server_options) do 21 | stack = <%= @module %>.API.Router.stack(config) 22 | 23 | Ace.HTTP.Service.start_link(stack, server_options) 24 | end 25 | 26 | # Utilities 27 | def set_json_payload(response, data) do 28 | response 29 | |> Raxx.set_header("content-type", "application/json") 30 | |> Raxx.set_body(Jason.encode!(data)) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/api/actions/not_found.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.API.Actions.NotFound do 2 | use Raxx.SimpleServer 3 | alias <%= @module %>.API 4 | 5 | @impl Raxx.SimpleServer 6 | def handle_request(_request, _state) do 7 | error = %{title: "Action not found"} 8 | 9 | response(:not_found) 10 | |> API.set_json_payload(%{errors: [error]}) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/api/actions/welcome_message.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.API.Actions.WelcomeMessage do 2 | use Raxx.SimpleServer 3 | alias <%= @module %>.API 4 | 5 | @impl Raxx.SimpleServer 6 | def handle_request(_request = %{method: :GET}, _state) do 7 | data = %{message: "Hello, Raxx!"} 8 | 9 | response(:ok) 10 | |> set_body(Jason.encode!(%{data: data})) 11 | end 12 | 13 | def handle_request(request = %{method: :POST}, _state) do 14 | case Jason.decode(request.body) do 15 | {:ok, %{"name" => name}} -> 16 | message = <%= @module %>.welcome_message(name) 17 | data = %{message: message} 18 | 19 | response(:ok) 20 | |> API.set_json_payload(%{data: data}) 21 | 22 | {:ok, _} -> 23 | error = %{title: "Missing required data parameter 'name'"} 24 | 25 | response(:bad_request) 26 | |> API.set_json_payload(%{errors: [error]}) 27 | 28 | {:error, _} -> 29 | error = %{title: "Could not decode request data"} 30 | 31 | response(:unsupported_media_type) 32 | |> API.set_json_payload(%{errors: [error]}) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/api/router.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.API.Router do 2 | use Raxx.Router 3 | alias <%= @module %>.API.Actions 4 | 5 | def stack(config) do 6 | Raxx.Stack.new( 7 | [ 8 | # Add global middleware here. 9 | ], 10 | {__MODULE__, config} 11 | ) 12 | end 13 | 14 | # Call GreetUser and in WWW dir AND call into lib 15 | section [{Raxx.Logger, Raxx.Logger.setup(level: :info)}], [ 16 | {%{path: []}, Actions.WelcomeMessage}, 17 | ] 18 | 19 | section [{Raxx.Logger, Raxx.Logger.setup(level: :debug)}], [ 20 | {_, Actions.NotFound} 21 | ] 22 | end 23 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/application.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_type, _args) do 7 | cleartext_options = [port: port(), cleartext: true] 8 | 9 | secure_options = [ 10 | port: secure_port(), 11 | certfile: certificate_path(), 12 | keyfile: certificate_key_path() 13 | ] 14 | 15 | children = [ 16 | <%= if @ecto do %> 17 | <%= @module %>.Repo, 18 | <% end %> 19 | {<%= @module %>.<%= if @api do %>API<% else %>WWW<% end %>, [cleartext_options]}, 20 | {<%= @module %>.<%= if @api do %>API<% else %>WWW<% end %>, [secure_options]}, 21 | <%= if @node_assets do %> 22 | Supervisor.child_spec({Task, fn() -> System.cmd("npm", ["run", "watch:js"], cd: "lib/<%= @name %>/www") end}, id: :watch_js), 23 | Supervisor.child_spec({Task, fn() -> System.cmd("npm", ["run", "watch:css"], cd: "lib/<%= @name %>/www") end}, id: :watch_css), 24 | <% end %> 25 | ] 26 | 27 | opts = [strategy: :one_for_one, name: <%= @module %>.Supervisor] 28 | Supervisor.start_link(children, opts) 29 | end 30 | 31 | defp port() do 32 | with raw when is_binary(raw) <- System.get_env("PORT"), {port, ""} = Integer.parse(raw) do 33 | port 34 | else 35 | _ -> 8080 36 | end 37 | end 38 | 39 | defp secure_port() do 40 | with raw when is_binary(raw) <- System.get_env("SECURE_PORT"), 41 | {secure_port, ""} = Integer.parse(raw) do 42 | secure_port 43 | else 44 | _ -> 8443 45 | end 46 | end 47 | 48 | defp certificate_path() do 49 | Application.app_dir(:<%= @name %>, "priv/localhost/certificate.pem") 50 | end 51 | 52 | defp certificate_key_path() do 53 | Application.app_dir(:<%= @name %>, "priv/localhost/certificate_key.pem") 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/repo.ex.eex: -------------------------------------------------------------------------------- 1 | <%= if @ecto do %>defmodule <%= @module %>.Repo do 2 | use Ecto.Repo, 3 | otp_app: :<%= @name %>, 4 | adapter: Ecto.Adapters.Postgres 5 | 6 | def init(_type, config) do 7 | # SEE https://hexdocs.pm/ecto/Ecto.Repo.html#module-urls for details 8 | config = 9 | case System.get_env("DATABASE_URL") do 10 | not_set when not_set in [nil, ""] -> 11 | config 12 | url -> 13 | Keyword.put(config, :url, url) 14 | end 15 | {:ok, config} 16 | end 17 | end 18 | <% end %> 19 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.WWW do 2 | 3 | def child_spec([server_options]) do 4 | {:ok, port} = Keyword.fetch(server_options, :port) 5 | %{ 6 | id: {__MODULE__, port}, 7 | start: {__MODULE__, :start_link, [server_options]}, 8 | type: :supervisor 9 | } 10 | end 11 | 12 | @session_config Raxx.Session.config( 13 | key: "my_app_session", 14 | store: Raxx.Session.SignedCookie, 15 | secret_key_base: "<%= Raxx.Kit.generate_random(64) %>", 16 | salt: "<%= Raxx.Kit.generate_random(6) %>" 17 | ) 18 | 19 | # This works even if the reference file is not available at start up, 20 | # i.e. it will be generated by npm scripts. 21 | @external_resource "lib/<%= @name %>/www/public/main.css" 22 | @external_resource "lib/<%= @name %>/www/public/main.js" 23 | options = [source: Path.join(__DIR__, "www/public")] 24 | 25 | @static_setup (if(Mix.env() == :dev) do 26 | options 27 | else 28 | Raxx.Static.setup(options) 29 | end) 30 | 31 | def init() do 32 | %{session_config: @session_config} 33 | end 34 | 35 | def start_link(server_options) do 36 | start_link(init(), server_options) 37 | end 38 | 39 | def start_link(config, server_options) do 40 | stack = 41 | Raxx.Stack.new( 42 | [ 43 | {Raxx.Static, @static_setup} 44 | ], 45 | {__MODULE__.Router, config} 46 | ) 47 | 48 | Ace.HTTP.Service.start_link(stack, server_options) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/_DOTFILE.gitignore.eex: -------------------------------------------------------------------------------- 1 | <%= if @node_assets do %>node_modules 2 | public/*.js 3 | public/*.css 4 | <% end %> 5 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/actions/home_page.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.WWW.Actions.HomePage do 2 | use Raxx.SimpleServer 3 | use <%= @module %>.WWW.Layout, arguments: [:greeting, :csrf_token] 4 | alias Raxx.Session 5 | 6 | @impl Raxx.SimpleServer 7 | def handle_request(request = %{method: :GET}, state) do 8 | {:ok, session} = Session.extract(request, state.session_config) 9 | 10 | {csrf_token, session} = Session.get_csrf_token(session) 11 | {flash, session} = Session.pop_flash(session) 12 | 13 | greeting = <%= @module %>.welcome_message(session[:name]) 14 | 15 | response(:ok) 16 | |> Session.embed(session, state.session_config) 17 | |> render(greeting, csrf_token, flash: flash) 18 | end 19 | 20 | def handle_request(request = %{method: :POST}, state) do 21 | data = URI.decode_query(request.body) 22 | {:ok, session} = Session.extract(request, data["_csrf_token"], state.session_config) 23 | 24 | case data do 25 | %{"name" => name} -> 26 | session = 27 | session 28 | |> Map.put(:name, name) 29 | |> Session.put_flash(:info, "Successfully changed name") 30 | 31 | redirect("/") 32 | |> Session.embed(session, state.session_config) 33 | 34 | _ -> 35 | redirect("/") 36 | end 37 | end 38 | 39 | # Template helper functions. 40 | # Add shared helper functions to <%= @module %>.WWW.Layout. 41 | end 42 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/actions/home_page.html.eex.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%%= page_header(greeting) %> 4 |
5 | 6 | 10 | 11 |
12 |

Find out more

13 | 17 | <%%= javascript_variables greeting: greeting %> 18 | 23 |
24 |
25 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/actions/not_found_page.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.WWW.Actions.NotFoundPage do 2 | use Raxx.SimpleServer 3 | use <%= @module %>.WWW.Layout, 4 | arguments: [], 5 | optional: [title: "Nothing Here"] 6 | 7 | @impl Raxx.SimpleServer 8 | def handle_request(_request, _state) do 9 | response(:not_found) 10 | |> render() 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/actions/not_found_page.html.eex.eex: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%%= page_header("Nothing here!") %> 4 |

5 | <%%= home_page_link() %> 6 |

7 |
8 |
9 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/assets/main.js.eex: -------------------------------------------------------------------------------- 1 | <%= if @node_assets do %>window.app = { 2 | show: function (title) { 3 | console.log(title) 4 | } 5 | }<% end %> 6 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/assets/main.scss.eex: -------------------------------------------------------------------------------- 1 | <%= if @node_assets do %>/* Reset */ 2 | body { 3 | margin: 0; 4 | } 5 | 6 | /* Make everything a border-box, because why not? */ 7 | html { 8 | box-sizing: border-box; 9 | } 10 | *, *:before, *:after { 11 | box-sizing: inherit; 12 | } 13 | 14 | html { 15 | min-height: 100%; 16 | } 17 | 18 | body { 19 | font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 20 | /* Currently ems cause chrome bug misinterpreting rems on body element */ 21 | font-size: 1.6em; 22 | font-weight: 300; 23 | letter-spacing: .01em; 24 | line-height: 1.6; 25 | color: white; 26 | 27 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#45484d+0,000000+100 */ 28 | background: rgb(69,72,77); /* Old browsers */ 29 | background: -moz-linear-gradient(right , rgba(69,72,77,1) 0%, rgba(0,0,0,1) 100%); /* FF3.6-15 */ 30 | background: -webkit-linear-gradient(right, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%); /* Chrome10-25,Safari5.1-6 */ 31 | background: linear-gradient(to left, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 32 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#45484d', endColorstr='#000000',GradientType=0 ); /* IE6-9 */ 33 | 34 | } 35 | 36 | h1 { 37 | margin: 0; 38 | font-size: 6.6rem; 39 | font-weight: 300; 40 | line-height: 1.2; 41 | } 42 | 43 | .centered { 44 | margin-left: auto; 45 | margin-right: auto; 46 | max-width: 80rem; 47 | } 48 | 49 | .accent { 50 | border-left: #00ff9c 2px solid; 51 | margin: 50px; 52 | padding: 30px; 53 | } 54 | 55 | .flash { 56 | padding: 0.2em 1em; 57 | border-radius: 0.5em; 58 | } 59 | 60 | .flash.info { 61 | background-color: #0eb3c6; 62 | } 63 | 64 | .flash.warning { 65 | background-color: #e14026; 66 | } 67 | 68 | nav { 69 | letter-spacing: 1.5em 70 | } 71 | 72 | nav a { 73 | color: #00ff9c; 74 | letter-spacing: 0.05em 75 | }<% end %> 76 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/layout.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.WWW.Layout do 2 | use Raxx.View.Layout, 3 | optional: [flash: %{}, title: "<%= @name %>"] 4 | 5 | # Shared template helper functions. 6 | # Use `~E` or the `partial/3` macro to generate HTML safely. 7 | 8 | def display_date(date) do 9 | Date.to_iso8601(date) 10 | end 11 | 12 | def home_page_link() do 13 | ~E""" 14 | Home 15 | """ 16 | end 17 | 18 | partial(:page_header, [:title]) 19 | end 20 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/layout.html.eex.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%%= title %> 6 | 7 | 8 | 9 | 10 | 11 | <%%= if message = Map.get(flash, :info) do %> 12 |

<%%= message %>

13 | <%% end %> 14 | <%%= if message = Map.get(flash, :warning) do %> 15 |

<%%= message %>

16 | <%% end %> 17 | <%%= __content__ %> 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/package-lock.json.eex: -------------------------------------------------------------------------------- 1 | <%= if @node_assets do %>{ "lockfileVersion": 1 }<% end %> 2 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/package.json.eex: -------------------------------------------------------------------------------- 1 | <%= if @node_assets do %>{ 2 | "name": "<%= @name %>", 3 | "scripts": { 4 | "watch:js": "rollup -w -i assets/main.js -o public/main.js -f iife -n main", 5 | "watch:css": "node-sass -w assets/main.scss public/main.css", 6 | "prewatch:css": "node-sass assets/main.scss public/main.css" 7 | }, 8 | "devDependencies": { 9 | "rollup": "^0.67.1", 10 | "node-sass": "^4.10.0" 11 | } 12 | } 13 | <% end %> 14 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/page_header.html.eex.eex: -------------------------------------------------------------------------------- 1 |
2 |

<%%= title || "Raxx.Kit" %>

3 |
4 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CrowdHailer/raxx_kit/e8481952e752321cff0e73bbab0bf32468c21409/priv/template/lib/app_name/www/public/favicon.ico -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/public/main.css.eex: -------------------------------------------------------------------------------- 1 | <%= if !@node_assets do %>/* Reset */ 2 | body { 3 | margin: 0; 4 | } 5 | 6 | /* Make everything a border-box, because why not? */ 7 | html { 8 | box-sizing: border-box; 9 | } 10 | *, *:before, *:after { 11 | box-sizing: inherit; 12 | } 13 | 14 | html { 15 | min-height: 100%; 16 | } 17 | 18 | body { 19 | font-family: 'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 20 | /* Currently ems cause chrome bug misinterpreting rems on body element */ 21 | font-size: 1.6em; 22 | font-weight: 300; 23 | letter-spacing: .01em; 24 | line-height: 1.6; 25 | color: white; 26 | 27 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#45484d+0,000000+100 */ 28 | background: rgb(69,72,77); /* Old browsers */ 29 | background: -moz-linear-gradient(right , rgba(69,72,77,1) 0%, rgba(0,0,0,1) 100%); /* FF3.6-15 */ 30 | background: -webkit-linear-gradient(right, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%); /* Chrome10-25,Safari5.1-6 */ 31 | background: linear-gradient(to left, rgba(69,72,77,1) 0%,rgba(0,0,0,1) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */ 32 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#45484d', endColorstr='#000000',GradientType=0 ); /* IE6-9 */ 33 | 34 | } 35 | 36 | h1 { 37 | margin: 0; 38 | font-size: 6.6rem; 39 | font-weight: 300; 40 | line-height: 1.2; 41 | } 42 | 43 | .centered { 44 | margin-left: auto; 45 | margin-right: auto; 46 | max-width: 80rem; 47 | } 48 | 49 | .accent { 50 | border-left: #00ff9c 2px solid; 51 | margin: 50px; 52 | padding: 30px; 53 | } 54 | 55 | .flash { 56 | padding: 0.2em 1em; 57 | border-radius: 0.5em; 58 | } 59 | 60 | .flash.info { 61 | background-color: #0eb3c6; 62 | } 63 | 64 | .flash.warning { 65 | background-color: #e14026; 66 | } 67 | 68 | nav { 69 | letter-spacing: 1.5em 70 | } 71 | 72 | nav a { 73 | color: #00ff9c; 74 | letter-spacing: 0.05em 75 | }<% end %> 76 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/public/main.js.eex: -------------------------------------------------------------------------------- 1 | <%= if !@node_assets do %>window.app = { 2 | show: function (title) { 3 | console.log(title) 4 | } 5 | }<% end %> 6 | -------------------------------------------------------------------------------- /priv/template/lib/app_name/www/router.ex.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.WWW.Router do 2 | use Raxx.Router 3 | alias <%= @module %>.WWW.Actions 4 | 5 | section [{Raxx.Logger, Raxx.Logger.setup(level: :info)}], [ 6 | {%{path: []}, Actions.HomePage}, 7 | ] 8 | 9 | section [{Raxx.Logger, Raxx.Logger.setup(level: :debug)}], [ 10 | {_, Actions.NotFoundPage} 11 | ] 12 | end 13 | -------------------------------------------------------------------------------- /priv/template/mix.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :<%= @name %>, 7 | version: "0.1.0", 8 | elixir: "~> <%= System.version %>", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps(), 13 | aliases: aliases() 14 | ]<%= if @docker do %> 15 | |> Keyword.merge(custom_artifacts_directory_opts())<% end %> 16 | end 17 | 18 | def application do 19 | [extra_applications: [:logger], mod: {<%= @module %>.Application, []}] 20 | end 21 | 22 | defp elixirc_paths(:test), do: ["lib", "test/support"] 23 | defp elixirc_paths(_), do: ["lib"] 24 | 25 | defp deps do 26 | [ 27 | {:ace, "~> 0.18.6"}, 28 | {:raxx_logger, "~> 0.2.2"}, 29 | {:jason, "~> 1.0"},<%= if @api do %><% else %> 30 | {:raxx_view, "~> 0.1.7"}, 31 | {:raxx_static, "~> 0.8.3"}, 32 | {:raxx_session, "~> 0.2.0"},<% end %><%= if @exsync do %> 33 | {:exsync, "~> 0.2.3", only: :dev},<% end %><%= if @ecto do %> 34 | {:postgrex, ">= 0.0.0"}, 35 | {:ecto_sql, "~> 3.0.0"}<% end %> 36 | ] 37 | end 38 | 39 | defp aliases() do 40 | [<%= if @ecto do %> 41 | test: ["ecto.create --quiet", "ecto.migrate", "test"]<% end %> 42 | ] 43 | end 44 | <%= if @docker do %> 45 | # makes sure that if the project is run by docker-compose inside a container, 46 | # its artifacts won't pollute the host's project directory 47 | defp custom_artifacts_directory_opts() do 48 | case System.get_env("MIX_ARTIFACTS_DIRECTORY") do 49 | unset when unset in [nil, ""] -> 50 | [] 51 | directory -> 52 | [ 53 | build_path: Path.join(directory, "_build"), 54 | deps_path: Path.join(directory, "deps") 55 | ] 56 | end 57 | end<% end %> 58 | end 59 | -------------------------------------------------------------------------------- /priv/template/mix.lock: -------------------------------------------------------------------------------- 1 | %{} -------------------------------------------------------------------------------- /priv/template/priv/localhost/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPjCCAiYCCQDrSpjNLJXcbTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV 3 | SzEPMA0GA1UECAwGTG9uZG9uMQ8wDQYDVQQHDAZMb25kb24xHDAaBgNVBAoME1dv 4 | cmtzaG9wIDE0IExpbWl0ZWQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNzA0Mjkx 5 | NzEyMTJaFw0xODA0MjkxNzEyMTJaMGExCzAJBgNVBAYTAlVLMQ8wDQYDVQQIDAZM 6 | b25kb24xDzANBgNVBAcMBkxvbmRvbjEcMBoGA1UECgwTV29ya3Nob3AgMTQgTGlt 7 | aXRlZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A 8 | MIIBCgKCAQEAmNvyeepIRsAm6QuUezhFf3KXTygBoYX4oYMfXb6ZklDQ8QAT9Brs 9 | YUW1+QDDHPF3foDa+k4Mm8XL6/yHqQFutnhqqYysd/XovCG1ff7Vq0TpX7GAHGEv 10 | rTj/Q63xqVOmyZINgvi9TfRTIKZ5LIo6O0dV5LSv6cLTXa8bFBxybigTxL+HgzY0 11 | e3kQzuFSYLOxvLCd4j7YnTzOsYY8M49mNRDbja4SkDcRxqV0mJDUkUxayaDWXY06 12 | eY1RtiYHFeZQF/2iEKBnsm62VJcFiq/vnjNkc1SBxqIoXE7BrRe+yLi1TniMds8g 13 | GDUX0QqnhbuG/USuB0ev0VZNg+LiUYA/dQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB 14 | AQAJaUbgakfrvtbD84hqpCGe0LmfCbjUEE5NIpu/TEvTjDgnuPVhwF2VBcVT6w96 15 | YPL3hxt9DsMUsXapaD5v+rGOVJGReKWyl1JN1nqd2BRkYD++6AznOul5WepXOSHO 16 | mCQOVPV2C3M+OYEDgLf9dcrGvpPJdexLLpy/xR1s9ZiNHKYGAfXxU6Va1uUi60lh 17 | g2jjTNkYhthLiqygatEViZ25D/N7GBUtbCLf7YBRDPId5JnAd2sFI4vGeJJZMee/ 18 | RCWKyzC+ttY9AHClpUcLc9YdJUofFfwHjO+jFf1u19WfUxlC3GPh45hSFZ/g+Hz8 19 | OxDc8YfSk8VAjJHu2ap7G9+G 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /priv/template/priv/localhost/certificate_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAmNvyeepIRsAm6QuUezhFf3KXTygBoYX4oYMfXb6ZklDQ8QAT 3 | 9BrsYUW1+QDDHPF3foDa+k4Mm8XL6/yHqQFutnhqqYysd/XovCG1ff7Vq0TpX7GA 4 | HGEvrTj/Q63xqVOmyZINgvi9TfRTIKZ5LIo6O0dV5LSv6cLTXa8bFBxybigTxL+H 5 | gzY0e3kQzuFSYLOxvLCd4j7YnTzOsYY8M49mNRDbja4SkDcRxqV0mJDUkUxayaDW 6 | XY06eY1RtiYHFeZQF/2iEKBnsm62VJcFiq/vnjNkc1SBxqIoXE7BrRe+yLi1TniM 7 | ds8gGDUX0QqnhbuG/USuB0ev0VZNg+LiUYA/dQIDAQABAoIBAQCOfA9ExzbCBGEA 8 | wFOSnDxj9UvHdDI4/ulonBIDzyPVeFGbJAh1dRc8AMAEMEqvUwGgwLndsh0cor5X 9 | 5dgKmJQ7sHk0PDWTyHw9yWok3QMMl7q2AX26dnj7jfKbgquNu7TvlZ3UpMnIvWMz 10 | Pxoag2qOUQtmmWqUio99dzjVgULFHFOufcSfPsM1s71BhcZwcssG2JxmbjsG8r1w 11 | wR80p1VQjWzQdBe5Kgc+ZZZidf1SSW3W65zrdAV3tT9A78hmTdvhdXG9SOTu3Vt8 12 | Eab1eAewfds0FGJtUIzpJe6DCWFBnpAsGbWCf2CMkPRhWcLHVFN8jHO6KpKdqq0b 13 | bjXmZTAhAoGBAMZdXGqpFQWD73nVqrbdyZhtDU8VCAeT0d5XmlNQwvVPyPch50FL 14 | zDt2H9k6+Ko3p8wjSIWdKFvsqYuCFbBI8Lf4b3+Tu2Vi9ajFrnHyGUlB0X1Ny+nt 15 | iXpAUYCsGnp6hylzkNCGMQXQ55oDVjXr8/m/M8QpsGbAicu05HLuene5AoGBAMVF 16 | 0pyZNhjNBiJ/p/9AR1Rao8jWC5e70jxXdqX3kbWzobD1+qNEEP0klpnJGUIMASTb 17 | P99OrY5Fo1bxJecLjrwpQU63d90erAnirq7luIjQnXHAT6lg7CZX4uNW+P/iqc8p 18 | bvmcue6ptWf4HPtqtLpxXzbAlAA6SN5UDpFLbuudAoGAGaVOYnfTwO/K0Uyfkp7g 19 | BnXq55OHgztIQd+/kw/49LBJAjJ+7IE5OWLPQU2PgqpJZmoVYTjtU90oGmJKHY2A 20 | mbhj6fGWo8gEjLpqEE9Fl6QLypB5UZglUwnnv6QAlF8tBF3tlhgTVHYqy02tIrGL 21 | zHk83xqotNAlwJF1i6praPkCgYABMGGLlhTQY3P1A0X08OM9K+quzDN3r6cdu/04 22 | FNzo9nM0CNeA4mkjzXOm66JeVoovOa8R3nyHTf4lCQEMenJayfjdy5dKWuP4j0g0 23 | P6g0EuXQCLOyNqZVuNPiQOTxTeFuITbNBFfOi3FPdhxem48JTKOhRdnegntr85++ 24 | 2nCJtQKBgE9wMkBbQGKQxgV1VtNuLdhBPrCnvFvInX+/M5eaOLmg9IeDWER1vv5m 25 | kHbAa6t+jvi70ZQPrwMmAVT6YawpUuR7Ttaga/QMThw0dSvUuiPLXPno3ZZl9GM4 26 | OwKJHTSsqQf3jIT86Bm3wh375B8Dw0PPJUtraAkE4ioidTKuDnD4 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /priv/template/priv/localhost/certificate_signing_request.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIICpjCCAY4CAQAwYTELMAkGA1UEBhMCVUsxDzANBgNVBAgMBkxvbmRvbjEPMA0G 3 | A1UEBwwGTG9uZG9uMRwwGgYDVQQKDBNXb3Jrc2hvcCAxNCBMaW1pdGVkMRIwEAYD 4 | VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCY 5 | 2/J56khGwCbpC5R7OEV/cpdPKAGhhfihgx9dvpmSUNDxABP0GuxhRbX5AMMc8Xd+ 6 | gNr6Tgybxcvr/IepAW62eGqpjKx39ei8IbV9/tWrROlfsYAcYS+tOP9DrfGpU6bJ 7 | kg2C+L1N9FMgpnksijo7R1XktK/pwtNdrxsUHHJuKBPEv4eDNjR7eRDO4VJgs7G8 8 | sJ3iPtidPM6xhjwzj2Y1ENuNrhKQNxHGpXSYkNSRTFrJoNZdjTp5jVG2JgcV5lAX 9 | /aIQoGeybrZUlwWKr++eM2RzVIHGoihcTsGtF77IuLVOeIx2zyAYNRfRCqeFu4b9 10 | RK4HR6/RVk2D4uJRgD91AgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAQEAXlpo5GxS 11 | oAavAybmPwlPYq9UekjsucRUx7q8MdI9+6JFgVZkJ47NZeq1ynuFM0QFGWC3Sbyx 12 | q0yKD3/UHIItta/4nQev366jAfi2Se6IE9RYYcmly36joHUB0MtceLYEGwazJbaB 13 | yq5nI0wgLDlQ318Uh9g+rc0DAohuEm+Guvq5xIVOXrkidhlPOiSHcXtIgGzHbfnz 14 | oir5rhtRgpCgulFq18ZWeQpIZ1UvTz0QbPvnnUZiVDxQVvKHwdRAjtVoJmuD9fuz 15 | F1ZiqKQWe4nqEw5pJuCK5tBdquB9hlWxaJhs/sR4dhcrjvX+8VxbGGaLlJV2mcrA 16 | EheSjS4N675U3w== 17 | -----END CERTIFICATE REQUEST----- 18 | -------------------------------------------------------------------------------- /priv/template/priv/repo/migrations/_DOTFILE.gitignore.eex: -------------------------------------------------------------------------------- 1 | <%= if @ecto do %> 2 | # This .gitignore file makes sure the priv/repo/migrations/ directory gets picked up by git. 3 | # Empty directories are ignored by git. 4 | <% end %> 5 | -------------------------------------------------------------------------------- /priv/template/test/app_name/api/actions/welcome_message_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.API.Actions.WelcomeMessageTest do 2 | use ExUnit.Case 3 | 4 | alias <%= @module %>.API.Actions.WelcomeMessage 5 | 6 | test "returns welcome message for a name" do 7 | request = Raxx.request(:POST, "/") 8 | |> <%= @module %>.API.set_json_payload(%{name: "Fiona"}) 9 | 10 | response = WelcomeMessage.handle_request(request, %{}) 11 | 12 | assert response.status == 200 13 | assert {"content-type", "application/json"} in response.headers 14 | assert {:ok, %{"data" => %{"message" => message}}} = Jason.decode(response.body) 15 | assert message == "Hello, Fiona!" 16 | end 17 | 18 | test "returns bad request for bad payload" do 19 | request = Raxx.request(:POST, "/") 20 | |> <%= @module %>.API.set_json_payload(%{}) 21 | 22 | response = WelcomeMessage.handle_request(request, %{}) 23 | 24 | assert response.status == 400 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /priv/template/test/app_name/database_test.exs.eex: -------------------------------------------------------------------------------- 1 | <%= if @ecto do %>defmodule <%= @module %>.DatabaseTest do 2 | use <%= @module %>.RepoCase 3 | alias <%= @module %>.Repo 4 | 5 | test "connecting to the database" do 6 | assert {:ok, result} = Repo.query("SELECT 42") 7 | assert %{rows: [[42]]} = result 8 | :ok 9 | end 10 | end 11 | <% end %> 12 | -------------------------------------------------------------------------------- /priv/template/test/app_name/www/actions/home_page_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>.WWW.Actions.HomePageTest do 2 | use ExUnit.Case 3 | 4 | alias <%= @module %>.WWW.Actions.HomePage 5 | 6 | test "returns the Raxx.Kit home page" do 7 | request = Raxx.request(:GET, "/") 8 | 9 | response = HomePage.handle_request(request, <%= @module %>.WWW.init()) 10 | 11 | assert response.status == 200 12 | assert {"content-type", "text/html"} in response.headers 13 | assert String.contains?(IO.iodata_to_binary(response.body), "Raxx.Kit") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /priv/template/test/app_name_test.exs.eex: -------------------------------------------------------------------------------- 1 | defmodule <%= @module %>Test do 2 | use ExUnit.Case, async: true 3 | doctest <%= @module %> 4 | 5 | setup %{} do 6 | # OS will assign a free port when service is started with port 0. 7 | {:ok, service} = <%= @module %>.<%= if @api do %>API<% else %>WWW<% end %>.start_link(port: 0, cleartext: true) 8 | {:ok, port} = Ace.HTTP.Service.port(service) 9 | 10 | {:ok, port: port} 11 | end 12 | 13 | test "Serves homepage", %{port: port} do 14 | assert {:ok, response} = :httpc.request('http://localhost:#{port}') 15 | assert {{_, 200, 'OK'}, _headers, _body} = response 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /priv/template/test/support/repo_case.ex.eex: -------------------------------------------------------------------------------- 1 | <%= if @ecto do %>defmodule <%= @module %>.RepoCase do 2 | use ExUnit.CaseTemplate 3 | # SEE https://hexdocs.pm/ecto/testing-with-ecto.html for more information 4 | 5 | using do 6 | quote do 7 | # alias <%= @module %>.Repo 8 | 9 | # import Ecto 10 | # import Ecto.Query 11 | # import <%= @module %>.RepoCase 12 | end 13 | end 14 | 15 | setup tags do 16 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(<%= @module %>.Repo) 17 | 18 | unless tags[:async] do 19 | Ecto.Adapters.SQL.Sandbox.mode(<%= @module %>.Repo, {:shared, self()}) 20 | end 21 | 22 | :ok 23 | end 24 | end 25 | <% end %> 26 | -------------------------------------------------------------------------------- /priv/template/test/test_helper.exs.eex: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | 3 | <%= if @ecto do %> 4 | Ecto.Adapters.SQL.Sandbox.mode(<%= @module %>.Repo, :manual) 5 | <% end %> 6 | -------------------------------------------------------------------------------- /scripts/docker_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eu 4 | # https://stackoverflow.com/a/246128/246337 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 6 | 7 | echo "" 8 | echo "## generating a demo project" 9 | echo "" 10 | 11 | pushd $DIR/.. 12 | mix compile 13 | mix raxx.new --docker --ecto demo 14 | popd 15 | 16 | pushd $DIR/../demo 17 | 18 | echo "" 19 | echo "## starting all services" 20 | echo "" 21 | 22 | docker-compose up -d 23 | 24 | echo "" 25 | echo "## getting demo project dependencies inside the container" 26 | echo "" 27 | 28 | docker-compose run demo mix deps.get 29 | 30 | echo "" 31 | echo "## running tests for demo project inside the container" 32 | echo "" 33 | 34 | docker-compose run demo mix test 35 | 36 | echo "" 37 | echo "## make sure the demo service is still alive" 38 | echo "" 39 | 40 | # if the demo project has problems starting, the service 41 | # will probably be dead by now 42 | docker-compose exec demo echo "I'm still alive!" 43 | 44 | # cleanup, for local docker_test runs 45 | docker-compose down 46 | 47 | popd 48 | -------------------------------------------------------------------------------- /test/raxx/kit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Raxx.KitTest do 2 | use ExUnit.Case 3 | doctest Raxx.Kit 4 | 5 | import Raxx.Kit 6 | 7 | describe "generate_random/1" do 8 | test "generates strings of the correct length" do 9 | assert 8 = String.length(generate_random(8)) 10 | assert 13 = String.length(generate_random(13)) 11 | assert 134 = String.length(generate_random(134)) 12 | end 13 | 14 | test "generates strings with no weird characters" do 15 | password = generate_random(333) 16 | assert Regex.match?(~r/^$/, "") 17 | assert Regex.match?(~r/^[0-9a-zA-Z_]+$/, password) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------