├── .example.env ├── .example.env.test ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── README.md ├── Rakefile ├── app.json ├── apps └── main │ ├── lib │ └── todo │ │ └── main │ │ ├── authorization.rb │ │ ├── authorizations │ │ └── tasks.rb │ │ ├── operations │ │ ├── sessions │ │ │ └── validate.rb │ │ ├── tasks │ │ │ ├── complete.rb │ │ │ ├── create.rb │ │ │ ├── find_task.rb │ │ │ └── validate.rb │ │ └── users │ │ │ ├── create.rb │ │ │ ├── encrypt_password.rb │ │ │ └── validate.rb │ │ ├── policies │ │ └── tasks_scope.rb │ │ ├── transaction.rb │ │ ├── transactions │ │ ├── complete_task.rb │ │ ├── create_task.rb │ │ ├── login.rb │ │ └── signup.rb │ │ ├── view │ │ ├── context.rb │ │ └── controller.rb │ │ └── views │ │ ├── sessions │ │ └── login.rb │ │ ├── tasks │ │ └── index.rb │ │ ├── users │ │ └── signup.rb │ │ └── welcome.rb │ ├── system │ ├── boot.rb │ └── todo │ │ └── main │ │ ├── container.rb │ │ ├── import.rb │ │ └── web.rb │ └── web │ ├── routes │ ├── example.rb │ ├── sessions.rb │ ├── signup.rb │ └── tasks.rb │ └── templates │ ├── layouts │ ├── _flash.html.slim │ ├── _navigation.html.slim │ └── application.html.slim │ ├── sessions │ └── login.html.slim │ ├── tasks │ └── index.html.slim │ ├── users │ └── signup.html.slim │ └── welcome.html.slim ├── bin ├── console └── setup ├── config.ru ├── db ├── migrate │ ├── 20171030020603_create_users.rb │ └── 20171030235001_create_tasks.rb ├── sample_data.rb ├── seed.rb └── structure.sql ├── lib ├── persistence │ ├── commands │ │ └── .keep │ └── relations │ │ ├── .keep │ │ ├── tasks.rb │ │ └── users.rb ├── todo │ ├── operation.rb │ ├── repositories │ │ ├── tasks_repo.rb │ │ └── users_repo.rb │ ├── repository.rb │ └── view │ │ ├── context.rb │ │ └── controller.rb └── types.rb ├── log └── .keep ├── spec ├── db_spec_helper.rb ├── factories │ ├── example.rb │ ├── task.rb │ └── user.rb ├── features │ └── main │ │ ├── complete_task_spec.rb │ │ ├── create_task_spec.rb │ │ ├── login_spec.rb │ │ ├── logout_spec.rb │ │ ├── signup_spec.rb │ │ └── tasks_list_spec.rb ├── lib │ └── todo │ │ └── repositories │ │ ├── tasks_repo_spec.rb │ │ └── users_repo_spec.rb ├── main │ └── lib │ │ └── todo │ │ └── main │ │ └── authorization_spec.rb ├── spec_helper.rb ├── support │ ├── db │ │ ├── factory.rb │ │ └── helpers.rb │ └── web │ │ ├── capybara.rb │ │ └── helpers.rb └── web_spec_helper.rb └── system ├── boot.rb ├── boot ├── monitor.rb ├── persistence.rb └── settings.rb └── todo ├── container.rb ├── import.rb └── web.rb /.example.env: -------------------------------------------------------------------------------- 1 | DATABASE_URL='postgres://{user}:{password}@{hostname}:{port}/dwr_todo_development' 2 | SESSION_SECRET='----put your secret key here----' 3 | -------------------------------------------------------------------------------- /.example.env.test: -------------------------------------------------------------------------------- 1 | DATABASE_URL='postgres://{user}:{password}@{hostname}:{port}/dwr_todo_test' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore app settings (copy to .example.env.* to check in example files) 2 | /.env* 3 | 4 | # RSpec 5 | /spec/examples.txt 6 | 7 | # Logs 8 | /log/*.log 9 | 10 | # Temp files 11 | tmp/* 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Include: 3 | - Rakefile 4 | - config.ru 5 | Exclude: 6 | - db/**/* 7 | - script/** 8 | - Gemfile 9 | - Rakefile 10 | 11 | Documentation: 12 | Enabled: false 13 | 14 | Style/EmptyMethod: 15 | EnforcedStyle: expanded 16 | SupportedStyles: 17 | - compact 18 | - expanded 19 | 20 | Layout/AlignHash: 21 | Enabled: false 22 | 23 | Layout/AlignParameters: 24 | EnforcedStyle: with_fixed_indentation 25 | 26 | Layout/MultilineMethodCallIndentation: 27 | EnforcedStyle: indented 28 | 29 | Metrics/BlockLength: 30 | Exclude: 31 | - spec/**/* 32 | - system/boot/* 33 | - apps/**/web/routes/* 34 | 35 | Metrics/LineLength: 36 | Exclude: 37 | - spec/**/* 38 | 39 | Metrics/ModuleLength: 40 | Exclude: 41 | - spec/**/* 42 | 43 | Style/LambdaCall: 44 | Enabled: false 45 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | dwr9 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.3.3 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake" 4 | 5 | # Web framework 6 | gem "dry-system", "~> 0.8" 7 | gem "dry-web", "~> 0.7" 8 | gem "dry-web-roda", "~> 0.7" 9 | gem "puma" 10 | gem "rack_csrf" 11 | 12 | gem "rack", ">= 2.0" 13 | gem "shotgun", ">= 0.9.2" 14 | 15 | # Database persistence 16 | gem "pg" 17 | gem "rom", "~> 4.0" 18 | gem "rom-sql", "~> 2.1" 19 | 20 | # Application dependencies 21 | gem "dry-matcher", "~> 0.6.0" 22 | gem "dry-monads", "~> 0.3" 23 | gem "dry-struct", "~> 0.3" 24 | gem "dry-transaction", "~> 0.10" 25 | gem "dry-types", "~> 0.12" 26 | gem "dry-validation", "~> 0.11" 27 | gem "dry-view", "~> 0.4" 28 | gem "slim" 29 | gem "bcrypt" 30 | gem "memoizable" 31 | 32 | group :development, :test do 33 | gem "pry-byebug", platform: :mri 34 | gem "rubocop", require: false 35 | end 36 | 37 | group :test do 38 | gem "capybara" 39 | gem "capybara-screenshot" 40 | gem "database_cleaner" 41 | gem "poltergeist" 42 | gem "rspec" 43 | gem "rom-factory", "~> 0.5" 44 | end 45 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.5.2) 5 | public_suffix (>= 2.0.2, < 4.0) 6 | ast (2.3.0) 7 | bcrypt (3.1.11) 8 | byebug (9.1.0) 9 | capybara (2.15.4) 10 | addressable 11 | mini_mime (>= 0.1.3) 12 | nokogiri (>= 1.3.3) 13 | rack (>= 1.0.0) 14 | rack-test (>= 0.5.4) 15 | xpath (~> 2.0) 16 | capybara-screenshot (1.0.17) 17 | capybara (>= 1.0, < 3) 18 | launchy 19 | cliver (0.3.2) 20 | coderay (1.1.2) 21 | concurrent-ruby (1.0.5) 22 | database_cleaner (1.6.1) 23 | diff-lcs (1.3) 24 | dry-auto_inject (0.4.4) 25 | dry-container (>= 0.3.4) 26 | dry-configurable (0.7.0) 27 | concurrent-ruby (~> 1.0) 28 | dry-container (0.6.0) 29 | concurrent-ruby (~> 1.0) 30 | dry-configurable (~> 0.1, >= 0.1.3) 31 | dry-core (0.3.4) 32 | concurrent-ruby (~> 1.0) 33 | dry-equalizer (0.2.0) 34 | dry-initializer (2.3.0) 35 | dry-logic (0.4.2) 36 | dry-container (~> 0.2, >= 0.2.6) 37 | dry-core (~> 0.2) 38 | dry-equalizer (~> 0.2) 39 | dry-matcher (0.6.0) 40 | dry-monads (0.3.1) 41 | dry-core 42 | dry-equalizer 43 | dry-monitor (0.0.3) 44 | dry-configurable (~> 0.5) 45 | dry-equalizer (~> 0.2) 46 | rouge (~> 2.0, >= 2.2.1) 47 | dry-struct (0.3.1) 48 | dry-configurable (~> 0.1) 49 | dry-core (~> 0.3) 50 | dry-equalizer (~> 0.2) 51 | dry-types (~> 0.9, >= 0.9.0) 52 | ice_nine (~> 0.11) 53 | dry-system (0.8.1) 54 | concurrent-ruby (~> 1.0) 55 | dry-auto_inject (>= 0.4.0) 56 | dry-configurable (~> 0.7, >= 0.7.0) 57 | dry-container (~> 0.6) 58 | dry-core (>= 0.3.1) 59 | dry-equalizer (~> 0.2) 60 | dry-struct (~> 0.3) 61 | inflecto (>= 0.0.2) 62 | dry-transaction (0.10.2) 63 | dry-container (>= 0.2.8) 64 | dry-matcher (>= 0.5.0) 65 | dry-monads (>= 0.0.1) 66 | wisper (>= 1.6.0) 67 | dry-types (0.12.1) 68 | concurrent-ruby (~> 1.0) 69 | dry-configurable (~> 0.1) 70 | dry-container (~> 0.3) 71 | dry-core (~> 0.2, >= 0.2.1) 72 | dry-equalizer (~> 0.2) 73 | dry-logic (~> 0.4, >= 0.4.2) 74 | inflecto (~> 0.0.0, >= 0.0.2) 75 | dry-validation (0.11.1) 76 | concurrent-ruby (~> 1.0) 77 | dry-configurable (~> 0.1, >= 0.1.3) 78 | dry-core (~> 0.2, >= 0.2.1) 79 | dry-equalizer (~> 0.2) 80 | dry-logic (~> 0.4, >= 0.4.0) 81 | dry-types (~> 0.12.0) 82 | dry-view (0.4.0) 83 | dry-configurable (~> 0.1) 84 | dry-core (~> 0.2) 85 | dry-equalizer (~> 0.2) 86 | tilt (~> 2.0) 87 | dry-web (0.7.1) 88 | dry-monitor 89 | dry-system (~> 0.5) 90 | dry-web-roda (0.9.1) 91 | dry-configurable (~> 0.2) 92 | inflecto (~> 0.0) 93 | roda (~> 2.14) 94 | roda-flow (~> 0.3) 95 | thor (~> 0.19) 96 | faker (1.8.4) 97 | i18n (~> 0.5) 98 | i18n (0.9.0) 99 | concurrent-ruby (~> 1.0) 100 | ice_nine (0.11.2) 101 | inflecto (0.0.2) 102 | launchy (2.4.3) 103 | addressable (~> 2.3) 104 | memoizable (0.4.2) 105 | thread_safe (~> 0.3, >= 0.3.1) 106 | method_source (0.9.0) 107 | mini_mime (0.1.4) 108 | mini_portile2 (2.3.0) 109 | nokogiri (1.8.2) 110 | mini_portile2 (~> 2.3.0) 111 | parallel (1.12.0) 112 | parser (2.4.0.0) 113 | ast (~> 2.2) 114 | pg (0.21.0) 115 | poltergeist (1.16.0) 116 | capybara (~> 2.1) 117 | cliver (~> 0.3.1) 118 | websocket-driver (>= 0.2.0) 119 | powerpack (0.1.1) 120 | pry (0.11.2) 121 | coderay (~> 1.1.0) 122 | method_source (~> 0.9.0) 123 | pry-byebug (3.5.0) 124 | byebug (~> 9.1) 125 | pry (~> 0.10) 126 | public_suffix (3.0.0) 127 | puma (3.10.0) 128 | rack (2.0.3) 129 | rack-test (0.7.0) 130 | rack (>= 1.0, < 3) 131 | rack_csrf (2.6.0) 132 | rack (>= 1.1.0) 133 | rainbow (2.2.2) 134 | rake 135 | rake (12.2.1) 136 | roda (2.29.0) 137 | rack 138 | roda-flow (0.3.1) 139 | rom (4.0.1) 140 | rom-changeset (~> 1.0) 141 | rom-core (~> 4.0, >= 4.0.1) 142 | rom-mapper (~> 1.0, >= 1.0.1) 143 | rom-repository (~> 2.0) 144 | rom-changeset (1.0.0) 145 | dry-core (~> 0.3, >= 0.3.1) 146 | rom-core (~> 4.0.0.rc) 147 | transproc (~> 1.0) 148 | rom-core (4.0.1) 149 | concurrent-ruby (~> 1.0) 150 | dry-container (~> 0.6) 151 | dry-core (~> 0.3) 152 | dry-equalizer (~> 0.2) 153 | dry-initializer (~> 2.0) 154 | dry-types (~> 0.12, >= 0.12.1) 155 | rom-mapper (~> 1.0) 156 | rom-factory (0.5.0) 157 | dry-configurable (~> 0.7) 158 | dry-core (~> 0.3, >= 0.3.1) 159 | dry-struct (~> 0.3) 160 | faker (~> 1.7) 161 | rom-core (~> 4.0) 162 | rom-mapper (1.0.1) 163 | dry-core (~> 0.3, >= 0.3.1) 164 | dry-equalizer (~> 0.2) 165 | transproc (~> 1.0) 166 | rom-repository (2.0.0) 167 | dry-struct (~> 0.3) 168 | rom-mapper (~> 1.0) 169 | rom-sql (2.1.0) 170 | dry-core (~> 0.3) 171 | dry-equalizer (~> 0.2) 172 | dry-types (~> 0.12, >= 0.12.1) 173 | rom-core (~> 4.0) 174 | sequel (>= 4.49) 175 | rouge (2.2.1) 176 | rspec (3.7.0) 177 | rspec-core (~> 3.7.0) 178 | rspec-expectations (~> 3.7.0) 179 | rspec-mocks (~> 3.7.0) 180 | rspec-core (3.7.0) 181 | rspec-support (~> 3.7.0) 182 | rspec-expectations (3.7.0) 183 | diff-lcs (>= 1.2.0, < 2.0) 184 | rspec-support (~> 3.7.0) 185 | rspec-mocks (3.7.0) 186 | diff-lcs (>= 1.2.0, < 2.0) 187 | rspec-support (~> 3.7.0) 188 | rspec-support (3.7.0) 189 | rubocop (0.51.0) 190 | parallel (~> 1.10) 191 | parser (>= 2.3.3.1, < 3.0) 192 | powerpack (~> 0.1) 193 | rainbow (>= 2.2.2, < 3.0) 194 | ruby-progressbar (~> 1.7) 195 | unicode-display_width (~> 1.0, >= 1.0.1) 196 | ruby-progressbar (1.9.0) 197 | sequel (5.2.0) 198 | shotgun (0.9.2) 199 | rack (>= 1.0) 200 | slim (3.0.8) 201 | temple (>= 0.7.6, < 0.9) 202 | tilt (>= 1.3.3, < 2.1) 203 | temple (0.8.0) 204 | thor (0.20.0) 205 | thread_safe (0.3.6) 206 | tilt (2.0.8) 207 | transproc (1.0.2) 208 | unicode-display_width (1.3.0) 209 | websocket-driver (0.7.0) 210 | websocket-extensions (>= 0.1.0) 211 | websocket-extensions (0.1.2) 212 | wisper (2.0.0) 213 | xpath (2.1.0) 214 | nokogiri (~> 1.3) 215 | 216 | PLATFORMS 217 | ruby 218 | 219 | DEPENDENCIES 220 | bcrypt 221 | capybara 222 | capybara-screenshot 223 | database_cleaner 224 | dry-matcher (~> 0.6.0) 225 | dry-monads (~> 0.3) 226 | dry-struct (~> 0.3) 227 | dry-system (~> 0.8) 228 | dry-transaction (~> 0.10) 229 | dry-types (~> 0.12) 230 | dry-validation (~> 0.11) 231 | dry-view (~> 0.4) 232 | dry-web (~> 0.7) 233 | dry-web-roda (~> 0.7) 234 | memoizable 235 | pg 236 | poltergeist 237 | pry-byebug 238 | puma 239 | rack (>= 2.0) 240 | rack_csrf 241 | rake 242 | rom (~> 4.0) 243 | rom-factory (~> 0.5) 244 | rom-sql (~> 2.1) 245 | rspec 246 | rubocop 247 | shotgun (>= 0.9.2) 248 | slim 249 | 250 | BUNDLED WITH 251 | 1.16.1 252 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alejandro E. Babio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dwr-todo-example 2 | 3 | ToDo example app built with [dry-web-roda](https://github.com/dry-rb/dry-web-roda). 4 | 5 | * Build this app [Step-by-step](http://dry-web-roda-todo-app.readthedocs.io/en/latest) 6 | * See this app working [in heroku](https://dwr-todo-example.herokuapp.com) 7 | 8 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 9 | 10 | ## In the step-by-step document you will find: 11 | 12 | * The folder structure of a dry-web-roda app 13 | * DB migrations, repositories and relations 14 | * Authentication 15 | * Authorization 16 | * The use of the Roda flow plugin to call dry-transactions and its output processing 17 | * Operations called in each step of the transactions 18 | * Scoping data for the current user by policies scopes (idea was taken from Pundit) 19 | * Input validation with dry-validation 20 | * Showing the current user in layout 21 | * Transactions for add and complete your tasks 22 | 23 | ## Isolating functionality using middleware 24 | 25 | In the branch [middleware-auth](https://github.com/alejandrobabio/dwr-todo-example/tree/middleware-auth) the **Authorization** functionality was moved to a middleware app, that is also a dry-web-roda app. More information in the commit's messages. 26 | 27 | ## Similar projects 28 | 29 | * [dry-rb/workshop-app](https://github.com/dry-rb/workshop-app) 30 | * [dry-web-blog](https://github.com/dry-rb/dry-web-blog) 31 | * [icelab.com.au](https://github.com/icelab/berg) This site is the precursor of [dry-web-roda](https://github.com/dry-rb/dry-web-roda), it could be using old versions of some gems, but still is a great site to read code and learn. 32 | 33 | ## Official documentation 34 | 35 | * [dry-rb](http://dry-rb.org/gems) 36 | * [ROM](http://rom-rb.org/4.0/learn) 37 | * [Roda](http://roda.jeremyevans.net) 38 | 39 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "pry-byebug" unless ENV["RACK_ENV"] == "production" 3 | require "rom/sql/rake_task" 4 | require "shellwords" 5 | require_relative "system/todo/container" 6 | 7 | begin 8 | require "rspec/core/rake_task" 9 | RSpec::Core::RakeTask.new :spec 10 | task default: [:spec] 11 | rescue LoadError 12 | end 13 | 14 | def db 15 | Todo::Container["persistence.db"] 16 | end 17 | 18 | def settings 19 | Todo::Container["settings"] 20 | end 21 | 22 | def database_uri 23 | require "uri" 24 | URI.parse(settings.database_url) 25 | end 26 | 27 | def postgres_env_vars(uri) 28 | {}.tap do |vars| 29 | vars["PGHOST"] = uri.host 30 | vars["PGPORT"] = uri.port.to_s if uri.port 31 | vars["PGUSER"] = uri.user if uri.user 32 | vars["PGPASSWORD"] = uri.password if uri.password 33 | end 34 | end 35 | 36 | namespace :db do 37 | task :setup do 38 | Todo::Container.init :persistence 39 | end 40 | 41 | desc "Print current database schema version" 42 | task version: :setup do 43 | version = 44 | if db.tables.include?(:schema_migrations) 45 | db[:schema_migrations].order(:filename).last[:filename] 46 | else 47 | "not available" 48 | end 49 | 50 | puts "Current schema version: #{version}" 51 | end 52 | 53 | desc "Create database" 54 | task :create do 55 | if system("which createdb", out: File::NULL) 56 | uri = database_uri 57 | system(postgres_env_vars(uri), "createdb #{Shellwords.escape(uri.path[1..-1])}") 58 | else 59 | puts "You must have Postgres installed to create a database" 60 | exit 1 61 | end 62 | end 63 | 64 | desc "Drop database" 65 | task :drop do 66 | if system("which dropdb", out: File::NULL) 67 | uri = database_uri 68 | system(postgres_env_vars(uri), "dropdb #{Shellwords.escape(uri.path[1..-1])}") 69 | else 70 | puts "You must have Postgres installed to drop a database" 71 | exit 1 72 | end 73 | end 74 | 75 | desc "Migrate database up to latest migration available" 76 | task :migrate do 77 | # Enhance the migration task provided by ROM 78 | 79 | # Once it finishes, dump the db structure 80 | Rake::Task["db:structure:dump"].execute 81 | 82 | # And print the current migration version 83 | Rake::Task["db:version"].execute 84 | end 85 | 86 | namespace :structure do 87 | desc "Dump database structure to db/structure.sql" 88 | task :dump do 89 | if system("which pg_dump", out: File::NULL) 90 | uri = database_uri 91 | system(postgres_env_vars(uri), "pg_dump -s -x -O #{Shellwords.escape(uri.path[1..-1])}", out: "db/structure.sql") 92 | else 93 | puts "You must have pg_dump installed to dump the database structure" 94 | end 95 | end 96 | end 97 | 98 | desc "Load seed data into the database" 99 | task :seed do 100 | seed_data = File.join("db", "seed.rb") 101 | load(seed_data) if File.exist?(seed_data) 102 | end 103 | 104 | desc "Load a small, representative set of data so that the application can start in a useful state (for development)." 105 | task :sample_data do 106 | sample_data = File.join("db", "sample_data.rb") 107 | load(sample_data) if File.exist?(sample_data) 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dry-web-roda TODO example app", 3 | "description": "A simple TODO app built with dry-web-roda", 4 | "repository": "https://github.com/alejandrobabio/dwr-todo-example", 5 | "keywords": ["dry-rb", "rom", "roda", "dry-web-roda"], 6 | "env": { 7 | "RACK_ENV": "production", 8 | "LANG": "en_US.UTF-8", 9 | "SESSION_SECRET": { 10 | "description": "The secret key", 11 | "generator": "secret" 12 | } 13 | }, 14 | "addons": ["heroku-postgresql:hobby-dev"], 15 | "buildpacks": [ 16 | { 17 | "url": "heroku/ruby" 18 | } 19 | ], 20 | "scripts": { 21 | "postdeploy": "bundle exec rake db:migrate" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/authorization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todo 4 | module Main 5 | class Authorization 6 | def call(user, request) 7 | name = Inflecto.camelize( 8 | request.path.split('/').reject(&:empty?).first 9 | ) 10 | Object.const_get("Todo::Main::Authorizations::#{name}") 11 | .new.(user, request) 12 | rescue NameError 13 | false 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/authorizations/tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todo 4 | module Main 5 | module Authorizations 6 | class Tasks 7 | def call(user, _request) 8 | user 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/sessions/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | require 'todo/main/import' 5 | require 'bcrypt' 6 | 7 | module Todo 8 | module Main 9 | module Operations 10 | module Sessions 11 | class Validate < Todo::Operation 12 | include Import['core.repositories.users_repo'] 13 | 14 | def call(attrs) 15 | email, password = attrs.values_at('email', 'password') 16 | user = users_repo.users.where(email: email).one 17 | 18 | if user && BCrypt::Password.new(user.password_digest) == password 19 | Right(user) 20 | else 21 | Left('Invalid Credentials') 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/tasks/complete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | 5 | module Todo 6 | module Main 7 | module Operations 8 | module Tasks 9 | class Complete < Todo::Operation 10 | include Import['core.repositories.tasks_repo'] 11 | 12 | def call(id) 13 | result = tasks_repo.update(id, completed: true) 14 | Right(result) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/tasks/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | 5 | module Todo 6 | module Main 7 | module Operations 8 | module Tasks 9 | class Create < Todo::Operation 10 | include Import['core.repositories.tasks_repo'] 11 | 12 | def call(attrs) 13 | task = tasks_repo.create(attrs) 14 | Right(task) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/tasks/find_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | 5 | module Todo 6 | module Main 7 | module Operations 8 | module Tasks 9 | class FindTask < Todo::Operation 10 | include Import['policies.tasks_scope'] 11 | 12 | def call(hash) 13 | id = tasks_scope.(hash[:current_user]) 14 | .where(id: hash[:params][:id]).pluck(:id).first 15 | 16 | if id 17 | Right(id) 18 | else 19 | Left(:error) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/tasks/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | require 'dry/validation' 5 | 6 | module Todo 7 | module Main 8 | module Operations 9 | module Tasks 10 | class Validate < Todo::Operation 11 | Schema = Dry::Validation.Form do 12 | required(:description).filled(:str?, min_size?: 3) 13 | required(:user_id).filled(:int?) 14 | end.freeze 15 | private_constant :Schema 16 | 17 | def call(hash) 18 | attrs = hash[:params].merge(user_id: hash[:current_user].id) 19 | validation = Schema.(attrs) 20 | 21 | if validation.success? 22 | Right(validation.output) 23 | else 24 | Left(validation) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/users/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | 5 | module Todo 6 | module Main 7 | module Operations 8 | module Users 9 | class Create < Todo::Operation 10 | include Import['core.repositories.users_repo'] 11 | 12 | def call(attrs) 13 | user = users_repo.create(attrs) 14 | Right(user) 15 | end 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/users/encrypt_password.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | require 'dry/validation' 5 | require 'bcrypt' 6 | 7 | module Todo 8 | module Main 9 | module Operations 10 | module Users 11 | class EncryptPassword < Todo::Operation 12 | def call(args) 13 | password = args.delete(:password) 14 | output = args.merge( 15 | password_digest: BCrypt::Password.create(password) 16 | ) 17 | 18 | Right(output) 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/operations/users/validate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/operation' 4 | require 'dry/validation' 5 | 6 | module Todo 7 | module Main 8 | module Operations 9 | module Users 10 | class Validate < Todo::Operation 11 | EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i 12 | 13 | Schema = Dry::Validation.Form do 14 | required(:email).filled(format?: EMAIL_REGEX) 15 | required(:password).filled(:str?, min_size?: 6) 16 | end.freeze 17 | private_constant :Schema 18 | 19 | def call(*args) 20 | validation = Schema.(*args) 21 | 22 | if validation.success? 23 | Right(validation.output) 24 | else 25 | Left(validation) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/policies/tasks_scope.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/import' 4 | 5 | module Todo 6 | module Main 7 | module Policies 8 | class TasksScope 9 | include Import['core.repositories.tasks_repo'] 10 | 11 | def call(user) 12 | tasks_repo.tasks.where(user_id: user.id) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/transaction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # auto_register: false 4 | 5 | require 'dry/transaction' 6 | 7 | module Todo 8 | module Main 9 | class Transaction 10 | include Dry::Transaction(container: Container) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/transactions/complete_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/transaction' 4 | 5 | module Todo 6 | module Main 7 | module Transactions 8 | class CompleteTask < Transaction 9 | step :find_task, with: 'operations.tasks.find_task' 10 | step :complete, with: 'operations.tasks.complete' 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/transactions/create_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/transaction' 4 | 5 | module Todo 6 | module Main 7 | module Transactions 8 | class CreateTask < Transaction 9 | step :validate, with: 'operations.tasks.validate' 10 | step :persist, with: 'operations.tasks.create' 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/transactions/login.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/transaction' 4 | 5 | module Todo 6 | module Main 7 | module Transactions 8 | class Login < Transaction 9 | step :validate, with: 'operations.sessions.validate' 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/transactions/signup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/transaction' 4 | 5 | module Todo 6 | module Main 7 | module Transactions 8 | class Signup < Transaction 9 | step :validate, with: 'operations.users.validate' 10 | step :encrypt_password, with: 'operations.users.encrypt_password' 11 | step :persist, with: 'operations.users.create' 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/view/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/view/context' 4 | 5 | module Todo 6 | module Main 7 | module View 8 | class Context < Todo::View::Context 9 | def current_user 10 | self[:current_user] 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/view/controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # auto_register: false 4 | 5 | require 'slim' 6 | require 'dry/view/controller' 7 | require 'todo/main/container' 8 | 9 | module Todo 10 | module Main 11 | module View 12 | class Controller < Dry::View::Controller 13 | configure do |config| 14 | config.paths = [Container.root.join('web/templates')] 15 | config.context = Container['view.context'] 16 | config.layout = 'application' 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/views/sessions/login.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/view/controller' 4 | require 'todo/main/import' 5 | 6 | module Todo 7 | module Main 8 | module Views 9 | module Sessions 10 | class Login < View::Controller 11 | configure do |config| 12 | config.template = 'sessions/login' 13 | end 14 | 15 | expose :error 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/views/tasks/index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/view/controller' 4 | require 'todo/main/import' 5 | 6 | module Todo 7 | module Main 8 | module Views 9 | module Tasks 10 | class Index < Main::View::Controller 11 | include Import['policies.tasks_scope'] 12 | 13 | configure do |config| 14 | config.template = 'tasks/index' 15 | end 16 | 17 | private_expose :current_user 18 | 19 | expose :tasks do |current_user| 20 | tasks_scope.(current_user).to_a 21 | end 22 | 23 | expose :validation 24 | 25 | expose :new_task do |validation| 26 | description = validation.to_h[:description] 27 | Struct.new(:description).new(description) 28 | end 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/views/users/signup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/view/controller' 4 | require 'todo/main/import' 5 | 6 | module Todo 7 | module Main 8 | module Views 9 | module Users 10 | class Signup < View::Controller 11 | configure do |config| 12 | config.template = 'users/signup' 13 | end 14 | 15 | expose :new_user, :validation 16 | 17 | private 18 | 19 | def new_user(validation: {}) 20 | Struct.new(:email, :password).new(validation[:email], nil) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /apps/main/lib/todo/main/views/welcome.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/main/view/controller' 4 | 5 | module Todo 6 | module Main 7 | module Views 8 | class Welcome < View::Controller 9 | configure do |config| 10 | config.template = 'welcome' 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /apps/main/system/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'todo/main/container' 4 | 5 | Todo::Main::Container.finalize! 6 | 7 | require 'todo/main/web' 8 | -------------------------------------------------------------------------------- /apps/main/system/todo/main/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pathname' 4 | require 'dry/web/container' 5 | require 'dry/system/components' 6 | 7 | module Todo 8 | module Main 9 | class Container < Dry::Web::Container 10 | require root.join('system/todo/container') 11 | import core: Todo::Container 12 | 13 | configure do |config| 14 | config.root = 15 | Pathname(__FILE__).join('../../..').realpath.dirname.freeze 16 | config.logger = Todo::Container[:logger] 17 | config.default_namespace = 'todo.main' 18 | config.auto_register = %w[lib/todo/main] 19 | end 20 | 21 | load_paths! 'lib' 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /apps/main/system/todo/main/import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'container' 4 | 5 | module Todo 6 | module Main 7 | Import = Container.injector 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /apps/main/system/todo/main/web.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/web/roda/application' 4 | require_relative 'container' 5 | require 'memoizable' 6 | 7 | module Todo 8 | module Main 9 | class Web < Dry::Web::Roda::Application 10 | include Memoizable 11 | 12 | setting :public_routes 13 | configure do |config| 14 | config.container = Container 15 | config.routes = 'web/routes' 16 | config.public_routes = %w[/ /login /logout /signup] 17 | end 18 | 19 | opts[:root] = Pathname(__FILE__).join('../../..').realpath.dirname 20 | 21 | use Rack::Session::Cookie, key: 'todo.main.session', 22 | secret: self['core.settings'].session_secret 23 | 24 | use Rack::MethodOverride 25 | 26 | plugin :csrf, raise: true 27 | plugin :dry_view 28 | plugin :error_handler 29 | plugin :flash 30 | plugin :multi_route 31 | plugin :all_verbs 32 | 33 | route do |r| 34 | unless authorize 35 | flash[:alert] = 'Unauthorized!' 36 | r.redirect '/' 37 | end 38 | 39 | # Enable this after writing your first web/routes/ file 40 | r.multi_route 41 | 42 | r.root do 43 | r.view 'welcome' 44 | end 45 | end 46 | 47 | def current_user 48 | return unless session[:user_id] 49 | self.class['core.repositories.users_repo'] 50 | .users.by_pk(session[:user_id]).one 51 | end 52 | memoize :current_user 53 | 54 | def params_with_user(params) 55 | { params: params, current_user: current_user } 56 | end 57 | 58 | def hash_with_user(hash = {}) 59 | hash.merge(current_user: current_user) 60 | end 61 | 62 | # Request-specific options for dry-view context object 63 | def view_context_options 64 | { 65 | flash: flash, 66 | csrf_token: Rack::Csrf.token(request.env), 67 | csrf_metatag: Rack::Csrf.metatag(request.env), 68 | csrf_tag: Rack::Csrf.tag(request.env), 69 | current_user: current_user 70 | } 71 | end 72 | 73 | load_routes! 74 | 75 | private 76 | 77 | def config 78 | self.class.config 79 | end 80 | 81 | def authorize 82 | return true if config.public_routes.include? request.path 83 | self.class['authorization'].(current_user, request) 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /apps/main/web/routes/example.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define your routes like this: 3 | # 4 | # class Todo::Main::Web 5 | # route "example" do |r| 6 | # # Routes go here 7 | # end 8 | # end 9 | -------------------------------------------------------------------------------- /apps/main/web/routes/sessions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todo 4 | module Main 5 | class Web 6 | route 'login' do |r| 7 | r.get do 8 | r.view 'sessions.login' 9 | end 10 | 11 | r.post do 12 | r.resolve 'transactions.login' do |login| 13 | login.(r[:user]) do |m| 14 | m.success do |user| 15 | flash[:notice] = "Welcome back #{user.email}" 16 | session[:user_id] = user.id 17 | r.redirect '/' 18 | end 19 | m.failure do |error| 20 | r.view 'sessions.login', error: error 21 | end 22 | end 23 | end 24 | end 25 | end 26 | 27 | route 'logout' do |r| 28 | flash[:notice] = 'User logged out' 29 | session.clear 30 | r.redirect '/' 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/main/web/routes/signup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todo 4 | module Main 5 | class Web 6 | route 'signup' do |r| 7 | r.get do 8 | r.view 'users.signup' 9 | end 10 | 11 | r.post do 12 | r.resolve 'transactions.signup' do |signup| 13 | signup.(r[:user]) do |m| 14 | m.success do 15 | flash[:notice] = 'Welcome! Now you have log in credentials.' 16 | r.redirect '/' 17 | end 18 | m.failure do |validation| 19 | r.view 'users.signup', validation: validation 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /apps/main/web/routes/tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todo 4 | module Main 5 | class Web 6 | route 'tasks' do |r| 7 | r.is do 8 | r.get do 9 | r.view 'tasks.index', current_user: current_user 10 | end 11 | 12 | r.post do 13 | r.resolve 'transactions.create_task' do |create_task| 14 | create_task.(params_with_user(r[:task])) do |m| 15 | m.success do |_task| 16 | flash[:notice] = 'Task created!' 17 | r.redirect '/tasks' 18 | end 19 | m.failure do |validation| 20 | r.view 'tasks.index', **hash_with_user(validation: validation) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | 27 | r.is :id do |id| 28 | r.patch do 29 | r.resolve 'transactions.complete_task' do |complete_task| 30 | complete_task.(params_with_user(id: id)) do |m| 31 | m.success do |_task| 32 | flash[:notice] = "task id: #{id}, completed" 33 | r.redirect '/tasks' 34 | end 35 | m.failure do 36 | flash[:alert] = 'Unexpected error' 37 | r.view 'tasks.index', **hash_with_user 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /apps/main/web/templates/layouts/_flash.html.slim: -------------------------------------------------------------------------------- 1 | - if flash[:notice] 2 | .notice.alert.alert-success role='alert' 3 | = flash[:notice] 4 | button.close type="button" data-dismiss="alert" aria-label="Close" 5 | span aria-hidden="true" × 6 | 7 | - if flash[:alert] 8 | .alert.alert.alert-danger role='alert' 9 | = flash[:alert] 10 | button.close type="button" data-dismiss="alert" aria-label="Close" 11 | span aria-hidden="true" × 12 | -------------------------------------------------------------------------------- /apps/main/web/templates/layouts/_navigation.html.slim: -------------------------------------------------------------------------------- 1 | .navbar.navbar-fixed-top.navbar-inverse 2 | .navbar-inner 3 | .container-fluid 4 | .navbar-header 5 | button.navbar-toggle.collapsed type="button" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false" 6 | span.sr-only Toggle navigation 7 | span.icon-bar 8 | span.icon-bar 9 | span.icon-bar 10 | a.navbar-brand href="/" Todo 11 | 12 | ul.nav.navbar-nav 13 | li 14 | a href="/tasks" 15 | | Tasks 16 | 17 | ul.nav.navbar-nav.navbar-right 18 | - if current_user 19 | li 20 | a == current_user.email 21 | li 22 | a href="/logout" 23 | | Log Out 24 | - else 25 | li 26 | a href="/login" 27 | | Log In 28 | li 29 | a href="/signup" 30 | | Sign Up 31 | -------------------------------------------------------------------------------- /apps/main/web/templates/layouts/application.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html lang="en" 3 | head 4 | /! Mobile first 5 | meta name="viewport" content="width=device-width, initial-scale=1" 6 | 7 | /! Latest compiled and minified CSS 8 | link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" 9 | 10 | /! Optional theme 11 | link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous" 12 | 13 | script src="https://code.jquery.com/jquery-3.2.1.min.js" 14 | 15 | /! Latest compiled and minified JavaScript 16 | script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous" 17 | 18 | body style="padding-top: 60px" 19 | 20 | == render 'navigation' 21 | 22 | .container-fluid 23 | .row 24 | .col-md-8.col-md-offset-2 25 | 26 | == render 'flash' 27 | 28 | .action 29 | == yield 30 | -------------------------------------------------------------------------------- /apps/main/web/templates/sessions/login.html.slim: -------------------------------------------------------------------------------- 1 | h1 Log In 2 | 3 | div 4 | -if error 5 | = error 6 | br 7 | 8 | form action='/login' method='post' 9 | == csrf_tag 10 | .form-group 11 | label for='user_email' 12 | | Email 13 | input.form-control id='user_email' name='user[email]' type='text' 14 | .form-group 15 | label for='user_password' 16 | | Password 17 | input.form-control id='user_password' name='user[password]' type='password' 18 | input.btn.btn-default type='submit' value='Log in' 19 | -------------------------------------------------------------------------------- /apps/main/web/templates/tasks/index.html.slim: -------------------------------------------------------------------------------- 1 | h1 Tasks 2 | 3 | div 4 | -if validation 5 | - validation.messages.each do |key, all_messages| 6 | b = key.capitalize 7 | = ": #{all_messages.join(', ')}" 8 | br 9 | 10 | form action='/tasks' method='post' 11 | == csrf_tag 12 | input.form-control name='task[description]' placeholder='Add a new task' value=new_task.description 13 | 14 | br 15 | 16 | ul.list-group.tasks 17 | - tasks.each do |task| 18 | - contextual_type = task.completed ? 'info' : 'warning' 19 | li.list-group-item.task class="list-group-item-#{contextual_type}" 20 | span = task.description 21 | - if task.completed 22 | .pull-right 23 | | Completed 24 | - else 25 | form.pull-right action="/tasks/#{task.id}" method='post' 26 | == csrf_tag 27 | input type='hidden' name='_method' value='patch' 28 | input.btn.btn-default.btn-xs type='submit' value='Complete' 29 | -------------------------------------------------------------------------------- /apps/main/web/templates/users/signup.html.slim: -------------------------------------------------------------------------------- 1 | h1 Sign Up 2 | 3 | div 4 | - if validation 5 | - validation.messages.each do |key, all_messages| 6 | b = key.capitalize 7 | = ": #{all_messages.join(', ')}" 8 | |   9 | br 10 | 11 | form action='/signup' method='post' 12 | == csrf_tag 13 | .form-group 14 | label for='user_email' 15 | | Email 16 | input.form-control id='user_email' name='user[email]' type='text' value=new_user.email 17 | .form-group 18 | label for='user_password' 19 | | Password 20 | input.form-control id='user_password' name='user[password]' type='password' 21 | input.btn.btn-default type='submit' value='Sign Up' 22 | -------------------------------------------------------------------------------- /apps/main/web/templates/welcome.html.slim: -------------------------------------------------------------------------------- 1 | h1 Welcome to dry-web-roda! 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'dry/web/console' 6 | require_relative '../system/boot' 7 | 8 | Dry::Web::Console.start 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_ROOT = File.expand_path('../../', __FILE__) 5 | 6 | Dir.chdir(APP_ROOT) do 7 | # Set up your app for development here 8 | end 9 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'system/boot' 4 | run Todo::Web.freeze.app 5 | -------------------------------------------------------------------------------- /db/migrate/20171030020603_create_users.rb: -------------------------------------------------------------------------------- 1 | ROM::SQL.migration do 2 | change do 3 | create_table(:users) do 4 | primary_key :id 5 | String :email 6 | String :password_digest 7 | DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP 8 | DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171030235001_create_tasks.rb: -------------------------------------------------------------------------------- 1 | ROM::SQL.migration do 2 | change do 3 | create_table(:tasks) do 4 | primary_key :id 5 | String :description 6 | Boolean :completed 7 | foreign_key :user_id, :users 8 | DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP 9 | DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/sample_data.rb: -------------------------------------------------------------------------------- 1 | # Build your sample data here 2 | -------------------------------------------------------------------------------- /db/seed.rb: -------------------------------------------------------------------------------- 1 | # Build your seed data here 2 | -------------------------------------------------------------------------------- /db/structure.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 9.5.9 6 | -- Dumped by pg_dump version 9.5.9 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET client_encoding = 'UTF8'; 11 | SET standard_conforming_strings = on; 12 | SET check_function_bodies = false; 13 | SET client_min_messages = warning; 14 | SET row_security = off; 15 | 16 | -- 17 | -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - 18 | -- 19 | 20 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 21 | 22 | 23 | -- 24 | -- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - 25 | -- 26 | 27 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 28 | 29 | 30 | SET search_path = public, pg_catalog; 31 | 32 | SET default_tablespace = ''; 33 | 34 | SET default_with_oids = false; 35 | 36 | -- 37 | -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - 38 | -- 39 | 40 | CREATE TABLE schema_migrations ( 41 | filename text NOT NULL 42 | ); 43 | 44 | 45 | -- 46 | -- Name: tasks; Type: TABLE; Schema: public; Owner: - 47 | -- 48 | 49 | CREATE TABLE tasks ( 50 | id integer NOT NULL, 51 | description text, 52 | completed boolean, 53 | user_id integer, 54 | created_at timestamp without time zone DEFAULT now() NOT NULL, 55 | updated_at timestamp without time zone DEFAULT now() NOT NULL 56 | ); 57 | 58 | 59 | -- 60 | -- Name: tasks_id_seq; Type: SEQUENCE; Schema: public; Owner: - 61 | -- 62 | 63 | CREATE SEQUENCE tasks_id_seq 64 | START WITH 1 65 | INCREMENT BY 1 66 | NO MINVALUE 67 | NO MAXVALUE 68 | CACHE 1; 69 | 70 | 71 | -- 72 | -- Name: tasks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 73 | -- 74 | 75 | ALTER SEQUENCE tasks_id_seq OWNED BY tasks.id; 76 | 77 | 78 | -- 79 | -- Name: users; Type: TABLE; Schema: public; Owner: - 80 | -- 81 | 82 | CREATE TABLE users ( 83 | id integer NOT NULL, 84 | email text, 85 | password_digest text, 86 | created_at timestamp without time zone DEFAULT now() NOT NULL, 87 | updated_at timestamp without time zone DEFAULT now() NOT NULL 88 | ); 89 | 90 | 91 | -- 92 | -- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - 93 | -- 94 | 95 | CREATE SEQUENCE users_id_seq 96 | START WITH 1 97 | INCREMENT BY 1 98 | NO MINVALUE 99 | NO MAXVALUE 100 | CACHE 1; 101 | 102 | 103 | -- 104 | -- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 105 | -- 106 | 107 | ALTER SEQUENCE users_id_seq OWNED BY users.id; 108 | 109 | 110 | -- 111 | -- Name: id; Type: DEFAULT; Schema: public; Owner: - 112 | -- 113 | 114 | ALTER TABLE ONLY tasks ALTER COLUMN id SET DEFAULT nextval('tasks_id_seq'::regclass); 115 | 116 | 117 | -- 118 | -- Name: id; Type: DEFAULT; Schema: public; Owner: - 119 | -- 120 | 121 | ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); 122 | 123 | 124 | -- 125 | -- Name: schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - 126 | -- 127 | 128 | ALTER TABLE ONLY schema_migrations 129 | ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (filename); 130 | 131 | 132 | -- 133 | -- Name: tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: - 134 | -- 135 | 136 | ALTER TABLE ONLY tasks 137 | ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); 138 | 139 | 140 | -- 141 | -- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: - 142 | -- 143 | 144 | ALTER TABLE ONLY users 145 | ADD CONSTRAINT users_pkey PRIMARY KEY (id); 146 | 147 | 148 | -- 149 | -- Name: tasks_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 150 | -- 151 | 152 | ALTER TABLE ONLY tasks 153 | ADD CONSTRAINT tasks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id); 154 | 155 | 156 | -- 157 | -- PostgreSQL database dump complete 158 | -- 159 | 160 | -------------------------------------------------------------------------------- /lib/persistence/commands/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejandrobabio/dwr-todo-example/84667962f34c27deb3380c048aeb41a83c974006/lib/persistence/commands/.keep -------------------------------------------------------------------------------- /lib/persistence/relations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejandrobabio/dwr-todo-example/84667962f34c27deb3380c048aeb41a83c974006/lib/persistence/relations/.keep -------------------------------------------------------------------------------- /lib/persistence/relations/tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module Relations 5 | class Tasks < ROM::Relation[:sql] 6 | schema(:tasks, infer: true) do 7 | associations do 8 | belongs_to :user 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/persistence/relations/users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Persistence 4 | module Relations 5 | class Users < ROM::Relation[:sql] 6 | schema(:users, infer: true) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/todo/operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # auto_register: false 4 | 5 | require 'dry/transaction/operation' 6 | 7 | module Todo 8 | class Operation 9 | def self.inherited(subclass) 10 | subclass.include Dry::Transaction::Operation 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/todo/repositories/tasks_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/repository' 4 | 5 | module Todo 6 | module Repositories 7 | class TasksRepo < Todo::Repository[:tasks] 8 | commands :create, update: :by_pk, delete: :by_pk 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/todo/repositories/users_repo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'todo/repository' 4 | 5 | module Todo 6 | module Repositories 7 | class UsersRepo < Todo::Repository[:users] 8 | commands :create, update: :by_pk, delete: :by_pk 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/todo/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # auto_register: false 4 | 5 | require 'rom-repository' 6 | require 'todo/container' 7 | require 'todo/import' 8 | 9 | module Todo 10 | class Repository < ROM::Repository::Root 11 | include Import.args['persistence.rom'] 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/todo/view/context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todo 4 | module View 5 | class Context 6 | attr_reader :attrs 7 | 8 | def initialize(attrs = {}) 9 | @attrs = attrs 10 | end 11 | 12 | def csrf_token 13 | self[:csrf_token] 14 | end 15 | 16 | def csrf_metatag 17 | self[:csrf_metatag] 18 | end 19 | 20 | def csrf_tag 21 | self[:csrf_tag] 22 | end 23 | 24 | def flash 25 | self[:flash] 26 | end 27 | 28 | def flash? 29 | %i[notice alert].any? { |type| flash[type] } 30 | end 31 | 32 | def with(new_attrs) 33 | self.class.new(attrs.merge(new_attrs)) 34 | end 35 | 36 | private 37 | 38 | def [](name) 39 | attrs.fetch(name) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/todo/view/controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # auto_register: false 4 | 5 | require 'slim' 6 | require 'dry/view/controller' 7 | require 'todo/container' 8 | 9 | module Todo 10 | module View 11 | class Controller < Dry::View::Controller 12 | configure do |config| 13 | config.paths = [Container.root.join('web/templates')] 14 | config.context = Container['view.context'] 15 | config.layout = 'application' 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry-struct' 4 | require 'dry-types' 5 | 6 | module Types 7 | include Dry::Types.module 8 | end 9 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alejandrobabio/dwr-todo-example/84667962f34c27deb3380c048aeb41a83c974006/log/.keep -------------------------------------------------------------------------------- /spec/db_spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'spec_helper' 4 | 5 | Todo::Container.start :persistence 6 | 7 | Dir[SPEC_ROOT.join('support/db/*.rb').to_s].each(&method(:require)) 8 | Dir[SPEC_ROOT.join('shared/db/*.rb').to_s].each(&method(:require)) 9 | 10 | require 'database_cleaner' 11 | DatabaseCleaner[:sequel, connection: Test::DatabaseHelpers.db].strategy = :truncation 12 | 13 | RSpec.configure do |config| 14 | config.include Test::DatabaseHelpers 15 | 16 | config.before :suite do 17 | DatabaseCleaner.clean_with :truncation 18 | end 19 | 20 | config.around :each do |example| 21 | DatabaseCleaner.cleaning do 22 | example.run 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/factories/example.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Define your factories here, e.g. 3 | # 4 | # Factory.define :article do |f| 5 | # f.sequence(:slug) { |i| "article-#{i}" } 6 | # f.title { fake(:lorem, :words, 5) } 7 | # f.body { fake(:lorem, :paragraphs, 1) } 8 | # end 9 | # 10 | # See https://github.com/rom-rb/rom-factory for more. 11 | -------------------------------------------------------------------------------- /spec/factories/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Factory.define :task do |f| 4 | f.sequence(:description) { |i| "description-#{i}" } 5 | f.association(:user) 6 | f.timestamps 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bcrypt' 4 | 5 | Factory.define :user do |f| 6 | f.sequence(:email) { |i| "user-#{i}@example.com" } 7 | f.password_digest(BCrypt::Password.create('secret')) 8 | f.timestamps 9 | end 10 | -------------------------------------------------------------------------------- /spec/features/main/complete_task_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'web_spec_helper' 4 | 5 | RSpec.feature 'Create Task' do 6 | scenario 'complete own task' do 7 | user = Factory[:user] 8 | Factory[:task, user_id: user.id] 9 | login user 10 | 11 | visit '/tasks' 12 | within('li.task') do 13 | click_on('Complete') 14 | end 15 | 16 | expect(find('li.task')).to have_content('Completed') 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/features/main/create_task_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'web_spec_helper' 4 | 5 | RSpec.feature 'Create Task' do 6 | before do 7 | login 8 | end 9 | 10 | scenario 'create task from index page' do 11 | visit '/tasks' 12 | fill_in 'Add a new task', with: 'My new task' 13 | page.submit find('form') 14 | 15 | expect(find('li.task')).to have_content('My new task') 16 | end 17 | 18 | scenario "can't create task with a short name" do 19 | visit '/tasks' 20 | fill_in 'Add a new task', with: 'My' 21 | page.submit find('form') 22 | 23 | expect(find('.action ul')).not_to have_content('My') 24 | expect(find('input').value).to eq 'My' 25 | expect(page).to have_content('Description: size cannot be less than 3') 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/features/main/login_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'web_spec_helper' 4 | 5 | RSpec.feature 'Log In' do 6 | scenario 'Can login with credentials' do 7 | user = Factory[:user] 8 | 9 | visit '/login' 10 | fill_in 'user[email]', with: user.email 11 | fill_in 'user[password]', with: 'secret' 12 | click_on 'Log in' 13 | 14 | expect(page).to have_content('Welcome back') 15 | expect(page).to have_content(user.email) 16 | end 17 | 18 | scenario "Can't login with wrong password" do 19 | user = Factory[:user] 20 | 21 | visit '/login' 22 | fill_in 'user[email]', with: user.email 23 | fill_in 'user[password]', with: 'invalid' 24 | click_on 'Log in' 25 | 26 | expect(page).to have_content('Invalid Credentials') 27 | end 28 | 29 | scenario "Can't login with wrong email" do 30 | Factory[:user] 31 | 32 | visit '/login' 33 | fill_in 'user[email]', with: 'wrong@email.com' 34 | fill_in 'user[password]', with: 'secret' 35 | click_on 'Log in' 36 | 37 | expect(page).to have_content('Invalid Credentials') 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/features/main/logout_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'web_spec_helper' 4 | 5 | RSpec.feature 'Log Out' do 6 | scenario 'Can logout' do 7 | user = Factory[:user] 8 | 9 | visit '/login' 10 | fill_in 'user[email]', with: user.email 11 | fill_in 'user[password]', with: 'secret' 12 | click_on 'Log in' 13 | 14 | expect(page).to have_content('Welcome back') 15 | expect(page).to have_content(user.email) 16 | 17 | visit '/logout' 18 | 19 | expect(page).not_to have_content(user.email) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/features/main/signup_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'web_spec_helper' 4 | 5 | RSpec.feature 'Sign Up' do 6 | scenario 'sign up as new user' do 7 | visit '/signup' 8 | fill_in 'user[email]', with: 'user@example.com' 9 | fill_in 'user[password]', with: 'secret' 10 | find('.action').click_on 'Sign Up' 11 | 12 | expect(page).to have_content('Welcome!') 13 | end 14 | 15 | scenario 'show error with invalid email' do 16 | visit '/signup' 17 | fill_in 'user[email]', with: 'user' 18 | fill_in 'user[password]', with: 'secret' 19 | find('.action').click_on 'Sign Up' 20 | 21 | expect(find('[name="user[email]"]').value).to eq 'user' 22 | expect(page).to have_content('Email: is in invalid format') 23 | end 24 | 25 | scenario 'show error with short password' do 26 | visit '/signup' 27 | fill_in 'user[email]', with: 'user@example.com' 28 | fill_in 'user[password]', with: 'nope' 29 | find('.action').click_on 'Sign Up' 30 | 31 | expect(page).to have_content('Password: size cannot be less than 6') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/features/main/tasks_list_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'web_spec_helper' 4 | 5 | RSpec.feature 'Tasks List' do 6 | scenario 'list tasks' do 7 | user = Factory[:user] 8 | own_tasks = (1..2).map { Factory[:task, user_id: user.id] } 9 | non_own_tasks = (1..2).map { Factory[:task] } 10 | login user 11 | visit '/tasks' 12 | own_tasks.each do |task| 13 | expect(page).to have_content(task.description) 14 | end 15 | non_own_tasks.each do |task| 16 | expect(page).not_to have_content(task.description) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/lib/todo/repositories/tasks_repo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'db_spec_helper' 4 | require 'todo/repositories/tasks_repo' 5 | 6 | RSpec.describe Todo::Repositories::TasksRepo do 7 | let(:repo) { Todo::Container['repositories.tasks_repo'] } 8 | 9 | it 'repo access to existing tasks' do 10 | 3.times { Factory[:task] } 11 | expect(repo.tasks.count).to eq 3 12 | end 13 | 14 | it 'create a task' do 15 | expect do 16 | repo.create(description: 'test task') 17 | end.to change(repo.tasks, :count).from(0).to(1) 18 | end 19 | 20 | it 'update a task' do 21 | task = repo.create(description: 'test task') 22 | repo.update(task.id, description: 'updated') 23 | expect(repo.tasks.first.description).to eq 'updated' 24 | end 25 | 26 | it 'delete a task' do 27 | task = repo.create(description: 'test task') 28 | expect do 29 | repo.delete(task.id) 30 | end.to change(repo.tasks, :count).from(1).to(0) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/lib/todo/repositories/users_repo_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'db_spec_helper' 4 | require 'todo/repositories/users_repo' 5 | 6 | RSpec.describe Todo::Repositories::UsersRepo do 7 | let(:repo) { Todo::Container['repositories.users_repo'] } 8 | 9 | it 'repo access to existing users' do 10 | 3.times { Factory[:user] } 11 | expect(repo.users.count).to eq 3 12 | end 13 | 14 | it 'create a user' do 15 | expect do 16 | Factory[:user] 17 | end.to change(repo.users, :count).from(0).to(1) 18 | end 19 | 20 | it 'update a user' do 21 | user = Factory[:user] 22 | repo.update(user.id, password_digest: 'new secret') 23 | expect(repo.users.first.password_digest).to eq 'new secret' 24 | end 25 | 26 | it 'delete a user' do 27 | user = Factory[:user] 28 | expect do 29 | repo.delete(user.id) 30 | end.to change(repo.users, :count).from(1).to(0) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/main/lib/todo/main/authorization_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | require_relative '../../../../../apps/main/lib/todo/main/authorization' 5 | 6 | module Todo 7 | module Main 8 | module Authorizations 9 | class MyResources 10 | def call(*) 11 | end 12 | end 13 | end 14 | end 15 | end 16 | 17 | RSpec.describe Todo::Main::Authorization do 18 | it 'returns a call response of Authorization class' do 19 | user = double('user') 20 | request = double('request', path: '/my_resources/my_path') 21 | my_resources = instance_double('Todo::Main::Authorizations::MyResources') 22 | expect(my_resources).to receive(:call).with(user, request) 23 | .and_return('whatever') 24 | expect(Todo::Main::Authorizations::MyResources).to receive(:new) 25 | .and_return(my_resources) 26 | 27 | expect(subject.(user, request)).to eq 'whatever' 28 | end 29 | 30 | it 'returns false if it found an error in root path' do 31 | user = double('user') 32 | request = double('request', path: '/missing_resources/my_path') 33 | 34 | expect(subject.(user, request)).to be_falsey 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RACK_ENV'] = 'test' 4 | 5 | require 'pry-byebug' 6 | 7 | SPEC_ROOT = Pathname(__FILE__).dirname 8 | 9 | Dir[SPEC_ROOT.join('support/*.rb').to_s].each(&method(:require)) 10 | Dir[SPEC_ROOT.join('shared/*.rb').to_s].each(&method(:require)) 11 | 12 | require SPEC_ROOT.join('../system/todo/container') 13 | 14 | RSpec.configure do |config| 15 | config.disable_monkey_patching! 16 | 17 | config.expect_with :rspec do |expectations| 18 | # This option will default to `true` in RSpec 4. 19 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 20 | end 21 | 22 | config.mock_with :rspec do |mocks| 23 | # Prevents you from mocking or stubbing a method that does not exist on a 24 | # real object. This is generally recommended, and will default to `true` 25 | # in RSpec 4. 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | # These two settings work together to allow you to limit a spec run to 30 | # individual examples or groups you care about by tagging them with `:focus` 31 | # metadata. When nothing is tagged with `:focus`, all examples get run. 32 | config.filter_run :focus 33 | config.run_all_when_everything_filtered = true 34 | 35 | # Allows RSpec to persist some state between runs in order to support the 36 | # `--only-failures` and `--next-failure` CLI options. 37 | config.example_status_persistence_file_path = 'spec/examples.txt' 38 | 39 | # Many RSpec users commonly either run the entire suite or an individual 40 | # file, and it's useful to allow more verbose output when running an 41 | # individual spec file. 42 | if config.files_to_run.one? 43 | # Use the documentation formatter for detailed output, unless a formatter 44 | # has already been configured (e.g. via a command-line flag). 45 | config.default_formatter = 'doc' 46 | end 47 | 48 | # Print the 10 slowest examples and example groups at the end of the spec 49 | # run, to help surface which specs are running particularly slow. 50 | config.profile_examples = 10 51 | 52 | # Run specs in random order to surface order dependencies. If you find an 53 | # order dependency and want to debug it, you can fix the order by providing 54 | # the seed, which is printed after each run. 55 | # --seed 1234 56 | config.order = :random 57 | 58 | # Seed global randomization in this process using the `--seed` CLI option. 59 | # Setting this allows you to use `--seed` to deterministically reproduce 60 | # test failures related to randomization by passing the same `--seed` value 61 | # as the one that triggered the failure. 62 | Kernel.srand config.seed 63 | end 64 | -------------------------------------------------------------------------------- /spec/support/db/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rom-factory' 4 | require_relative 'helpers' 5 | 6 | Factory = ROM::Factory.configure do |config| 7 | config.rom = Test::DatabaseHelpers.rom 8 | end 9 | 10 | Dir[Pathname(__FILE__).dirname.join('../../factories/**/*.rb')].each(&method(:require)) 11 | -------------------------------------------------------------------------------- /spec/support/db/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module DatabaseHelpers 5 | module_function 6 | 7 | def rom 8 | Todo::Container['persistence.rom'] 9 | end 10 | 11 | def db 12 | Todo::Container['persistence.db'] 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/support/web/capybara.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | class Session 5 | def submit(element) 6 | Capybara::RackTest::Form.new(driver, element.native).submit({}) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/web/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module WebHelpers 5 | module_function 6 | 7 | def app 8 | Todo::Web.app 9 | end 10 | 11 | def login(user = Factory[:user]) 12 | visit '/login' 13 | fill_in 'user[email]', with: user.email 14 | fill_in 'user[password]', with: 'secret' 15 | click_on 'Log in' 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/web_spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'db_spec_helper' 4 | 5 | require 'rack/test' 6 | require 'capybara/rspec' 7 | require 'capybara-screenshot/rspec' 8 | require 'capybara/poltergeist' 9 | 10 | Dir[SPEC_ROOT.join('support/web/*.rb').to_s].each(&method(:require)) 11 | Dir[SPEC_ROOT.join('shared/web/*.rb').to_s].each(&method(:require)) 12 | 13 | require SPEC_ROOT.join('../system/boot').realpath 14 | 15 | Capybara.app = Test::WebHelpers.app 16 | Capybara.server_port = 3001 17 | Capybara.save_path = "#{File.dirname(__FILE__)}/../tmp/capybara-screenshot" 18 | Capybara.javascript_driver = :poltergeist 19 | Capybara::Screenshot.prune_strategy = { keep: 10 } 20 | 21 | Capybara.register_driver :poltergeist do |app| 22 | Capybara::Poltergeist::Driver.new( 23 | app, 24 | js_errors: false, 25 | phantomjs_logger: File.open(SPEC_ROOT.join('../log/phantomjs.log'), 'w'), 26 | phantomjs_options: %w[--load-images=no], 27 | window_size: [1600, 768] 28 | ) 29 | end 30 | 31 | RSpec.configure do |config| 32 | config.include Rack::Test::Methods, type: :request 33 | config.include Rack::Test::Methods, Capybara::DSL, type: :feature 34 | config.include Test::WebHelpers 35 | 36 | config.before :suite do 37 | Test::WebHelpers.app.freeze 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /system/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require 'pry-byebug' 5 | rescue LoadError 6 | nil 7 | end 8 | 9 | require_relative 'todo/container' 10 | 11 | Todo::Container.finalize! 12 | 13 | # Load sub-apps 14 | app_paths = Pathname(__FILE__).dirname.join('../apps').realpath.join('*') 15 | Dir[app_paths].each do |f| 16 | require "#{f}/system/boot" 17 | end 18 | 19 | require 'todo/web' 20 | -------------------------------------------------------------------------------- /system/boot/monitor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Todo::Container.boot :monitor do 4 | init do 5 | require 'dry/monitor' 6 | end 7 | 8 | start do 9 | Dry::Monitor::SQL::Logger.new(logger).subscribe(notifications) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /system/boot/persistence.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Todo::Container.boot :persistence, namespace: true do |system| 4 | init do 5 | require 'sequel' 6 | require 'rom' 7 | require 'rom/sql' 8 | 9 | use :monitor, :settings 10 | 11 | ROM::SQL.load_extensions :postgres 12 | 13 | Sequel.database_timezone = :utc 14 | Sequel.application_timezone = :local 15 | 16 | rom_config = ROM::Configuration.new( 17 | :sql, 18 | system[:settings].database_url, 19 | extensions: %i[error_sql pg_array pg_json] 20 | ) 21 | 22 | rom_config.plugin :sql, relations: :instrumentation do |plugin_config| 23 | plugin_config.notifications = notifications 24 | end 25 | 26 | rom_config.plugin :sql, relations: :auto_restrictions 27 | 28 | register 'config', rom_config 29 | register 'db', rom_config.gateways[:default].connection 30 | end 31 | 32 | start do 33 | config = container['persistence.config'] 34 | config.auto_registration system.root.join('lib/persistence') 35 | 36 | register 'rom', ROM.container(config) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /system/boot/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Todo::Container.boot :settings, from: :system do 4 | before :init do 5 | ::Kernel.require 'types' 6 | end 7 | 8 | settings do 9 | key :session_secret, Types::Strict::String.constrained(filled: true) 10 | key :database_url, Types::Strict::String.constrained(filled: true) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /system/todo/container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/web/container' 4 | require 'dry/system/components' 5 | 6 | module Todo 7 | class Container < Dry::Web::Container 8 | configure do 9 | config.name = :todo 10 | config.listeners = true 11 | config.default_namespace = 'todo' 12 | config.auto_register = %w[lib/todo] 13 | end 14 | 15 | load_paths! 'lib' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /system/todo/import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'container' 4 | 5 | module Todo 6 | Import = Todo::Container.injector 7 | end 8 | -------------------------------------------------------------------------------- /system/todo/web.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/web/roda/application' 4 | require_relative 'container' 5 | 6 | module Todo 7 | class Web < Dry::Web::Roda::Application 8 | configure do |config| 9 | config.container = Container 10 | end 11 | 12 | plugin :error_handler 13 | 14 | route do |r| 15 | r.run Todo::Main::Web.freeze.app 16 | end 17 | 18 | error do |e| 19 | self.class[:rack_monitor].instrument(:error, exception: e) 20 | raise e 21 | end 22 | end 23 | end 24 | --------------------------------------------------------------------------------