├── .circleci └── config.yml ├── .config └── rubocop │ └── config.yml ├── .devcontainer ├── Dockerfile ├── compose.yaml └── devcontainer.json ├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── .node-version ├── .reek.yml ├── .ruby-version ├── CITATION.cff ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE.adoc ├── Procfile ├── README.adoc ├── Rakefile ├── app ├── action.rb ├── assets │ ├── css │ │ └── app.css │ ├── images │ │ └── icon.svg │ ├── js │ │ └── app.js │ └── pwa │ │ └── manifest.webmanifest ├── db │ ├── relation.rb │ ├── repository.rb │ └── struct.rb ├── templates │ └── layouts │ │ └── app.html.erb └── view.rb ├── bin ├── console ├── docker │ ├── build │ ├── console │ └── entrypoint ├── hanami ├── rake ├── render │ └── setup ├── rspec ├── rubocop └── setup ├── config.ru ├── config ├── app.rb ├── assets.js ├── initializers │ └── rack_attack.rb ├── puma.rb ├── routes.rb ├── settings.rb └── slices │ ├── health.rb │ ├── home.rb │ └── tasks.rb ├── lib └── hemo │ ├── refines │ └── actions │ │ └── response.rb │ └── types.rb ├── package-lock.json ├── package.json ├── public ├── .well-known │ └── security.txt ├── 404.html └── 500.html ├── slices ├── health │ ├── action.rb │ ├── actions │ │ └── show.rb │ ├── templates │ │ ├── layouts │ │ │ └── app.html.erb │ │ └── show.html.erb │ ├── view.rb │ └── views │ │ ├── context.rb │ │ └── show.rb ├── home │ ├── action.rb │ ├── actions │ │ └── show.rb │ ├── templates │ │ └── show.html.erb │ ├── view.rb │ └── views │ │ ├── context.rb │ │ └── show.rb └── tasks │ ├── action.rb │ ├── actions │ ├── create.rb │ ├── delete.rb │ ├── edit.rb │ ├── index.rb │ ├── new.rb │ ├── patch.rb │ ├── show.rb │ └── update.rb │ ├── assets │ ├── css │ │ └── app.css │ └── js │ │ └── app.js │ ├── config │ └── db │ │ ├── migrate │ │ ├── 20230820202820_create_users.rb │ │ └── 20230820202821_create_tasks.rb │ │ ├── seeds.rb │ │ └── structure.sql │ ├── db │ ├── relation.rb │ ├── repository.rb │ └── struct.rb │ ├── relations │ ├── task.rb │ └── user.rb │ ├── repositories │ ├── task.rb │ └── user.rb │ ├── templates │ ├── _description.html.erb │ ├── _rows.html.erb │ ├── _search.html.erb │ ├── _task.html.erb │ ├── _users.html.erb │ ├── edit.html.erb │ ├── index.html.erb │ ├── layouts │ │ └── app.html.erb │ ├── new.html.erb │ ├── search.html.erb │ └── show.html.erb │ ├── view.rb │ └── views │ ├── context.rb │ ├── edit.rb │ ├── helpers.rb │ ├── index.rb │ ├── new.rb │ ├── parts │ └── task.rb │ ├── scopes │ └── description.rb │ ├── search.rb │ └── show.rb └── spec ├── features ├── home_spec.rb └── tasks_spec.rb ├── hanami_helper.rb ├── lib └── hemo │ └── refines │ └── actions │ └── response_spec.rb ├── slices ├── health │ └── actions │ │ └── show_spec.rb └── tasks │ ├── actions │ ├── create_spec.rb │ ├── delete_spec.rb │ ├── edit_spec.rb │ ├── index_spec.rb │ ├── new_spec.rb │ ├── patch_spec.rb │ ├── show_spec.rb │ └── update_spec.rb │ ├── repositories │ └── task_spec.rb │ └── views │ ├── parts │ └── task_spec.rb │ ├── scopes │ └── description_spec.rb │ └── show_spec.rb ├── spec_helper.rb └── support ├── factories ├── task.rb └── user.rb └── shared_contexts ├── application.rb └── temp_dir.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | working_directory: ~/project 5 | docker: 6 | - image: bkuhlmann/alpine-ruby:latest 7 | environment: 8 | HANAMI_ENV: test 9 | DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres 10 | - image: postgres:latest 11 | environment: 12 | POSTGRES_PASSWORD: postgres 13 | steps: 14 | - run: 15 | name: Chromium Install 16 | command: apk add gcompat glib nss libxcb libgcc chromium 17 | 18 | - run: 19 | name: Chromium Start 20 | command: | 21 | export DISPLAY=:99 22 | chromedriver --url-base=/wd/hub & 23 | 24 | - run: 25 | name: Node Install 26 | command: apk add nodejs npm 27 | 28 | - checkout 29 | 30 | - restore_cache: 31 | name: Gems Restore 32 | keys: 33 | - gem-cache-{{.Branch}}-{{checksum "Gemfile.lock"}} 34 | - gem-cache- 35 | 36 | - run: 37 | name: Gems Install 38 | command: | 39 | gem update --system 40 | bundle config set path "vendor/bundle" 41 | bundle install 42 | 43 | - save_cache: 44 | name: Gems Store 45 | key: gem-cache-{{.Branch}}-{{checksum "Gemfile.lock"}} 46 | paths: 47 | - vendor/bundle 48 | 49 | - restore_cache: 50 | name: Packages Restore 51 | keys: 52 | - package-cache-{{.Branch}}-{{checksum "package.json"}} 53 | - package-cache- 54 | 55 | - run: 56 | name: Packages Install 57 | command: npm install 58 | 59 | - save_cache: 60 | name: Packages Store 61 | key: package-cache-{{.Branch}}-{{checksum "package.json"}} 62 | paths: 63 | - node_modules 64 | 65 | - run: 66 | name: Database Setup 67 | command: bin/hanami db prepare 68 | 69 | - run: 70 | name: Build 71 | command: | 72 | bin/hanami assets compile 73 | bundle exec rake 74 | 75 | - store_artifacts: 76 | name: SimpleCov Archive 77 | path: ~/project/coverage 78 | destination: coverage 79 | -------------------------------------------------------------------------------- /.config/rubocop/config.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | caliber: config/all.yml 3 | 4 | plugins: rubocop-sequel 5 | 6 | Metrics/MethodLength: 7 | Exclude: 8 | - "slices/tasks/views/helpers.rb" 9 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM bkuhlmann/alpine-ruby:latest 2 | -------------------------------------------------------------------------------- /.devcontainer/compose.yaml: -------------------------------------------------------------------------------- 1 | name: "hemo" 2 | 3 | services: 4 | app: 5 | build: 6 | context: .. 7 | dockerfile: .devcontainer/Dockerfile 8 | volumes: 9 | - ../..:/workspaces:cached 10 | environment: 11 | DATABASE_URL: postgres://postgres:postgres@postgres:5432/postgres 12 | command: sleep infinity 13 | depends_on: 14 | - postgres 15 | - redis 16 | 17 | postgres: 18 | image: postgres:latest 19 | restart: unless-stopped 20 | volumes: 21 | - postgres-data:/var/lib/postgresql/data 22 | environment: 23 | POSTGRES_USER: postgres 24 | POSTGRES_DB: postgres 25 | POSTGRES_PASSWORD: postgres 26 | 27 | redis: 28 | image: redis:latest 29 | restart: unless-stopped 30 | volumes: 31 | - redis-data:/data 32 | 33 | volumes: 34 | postgres-data: 35 | redis-data: 36 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hemo", 3 | "dockerComposeFile": "compose.yaml", 4 | "service": "app", 5 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 6 | "features": { 7 | "ghcr.io/devcontainers/features/github-cli:1": {} 8 | }, 9 | "containerEnv": { 10 | "CAPYBARA_SERVER_PORT": "45678", 11 | "REDIS_URL": "redis://redis:6379/1" 12 | }, 13 | "forwardPorts": [2300, 3000, 5432, 6379], 14 | "postCreateCommand": "bin/setup" 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Assets 2 | /node_modules/ 3 | 4 | # Bin 5 | /bin 6 | !/bin/console 7 | !/bin/docker/entrypoint 8 | 9 | # Bundler 10 | /.bundle 11 | 12 | # Development 13 | /.devcontainer 14 | 15 | # Docker 16 | /.dockerignore 17 | /Dockerfile* 18 | 19 | # Environment 20 | /.env* 21 | 22 | # Git 23 | /.git/ 24 | /.gitignore 25 | 26 | # GitHub 27 | /.github 28 | 29 | # Log 30 | /log/ 31 | 32 | # Temp 33 | /tmp/ 34 | 35 | # XDG 36 | /.config 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bkuhlmann] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Why 2 | 3 | 4 | ## How 5 | 6 | 7 | ## Notes 8 | 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | 4 | ## Screenshots/Screencasts 5 | 6 | 7 | ## Details 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ubuntu-latest 9 | env: 10 | HANAMI_ENV: test 11 | DATABASE_URL: postgres://postgres:postgres@localhost:5432/postgres 12 | 13 | services: 14 | postgres: 15 | image: postgres:latest 16 | env: 17 | POSTGRES_PASSWORD: postgres 18 | ports: 19 | - 5432:5432 20 | options: >- 21 | --health-cmd pg_isready 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | 26 | steps: 27 | - name: System Update 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install --no-install-recommends -y curl libjemalloc2 31 | 32 | - name: Chromium Setup 33 | uses: nanasess/setup-chromedriver@v2 34 | 35 | - name: Chromium Start 36 | run: | 37 | export DISPLAY=:99 38 | chromedriver --url-base=/wd/hub & 39 | 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | ref: ${{github.head_ref}} 45 | 46 | - name: Ruby Setup 47 | uses: ruby/setup-ruby@v1 48 | with: 49 | bundler-cache: true 50 | 51 | - name: Node Setup 52 | uses: actions/setup-node@v4 53 | with: 54 | cache: npm 55 | 56 | - name: Packages Install 57 | run: npm install 58 | 59 | - name: Database Setup 60 | run: bin/hanami db prepare 61 | 62 | - name: Build 63 | run: | 64 | bin/hanami assets compile 65 | bundle exec rake 66 | 67 | - name: SimpleCov Archive 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: coverage 71 | path: coverage 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | node_modules 3 | public/assets 4 | public/assets.json 5 | tmp 6 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 24.1.0 2 | -------------------------------------------------------------------------------- /.reek.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - tmp 3 | - vendor 4 | 5 | detectors: 6 | LongParameterList: 7 | enabled: false 8 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.4.4 2 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: Please use the following metadata when citing this project in your work. 3 | title: Hemo 4 | abstract: 5 | version: 0.0.0 6 | license: Hippocratic-2.1 7 | date-released: 2024-03-02 8 | authors: 9 | - family-names: Kuhlmann 10 | given-names: Brooke 11 | affiliation: Alchemists 12 | orcid: https://orcid.org/0000-0002-5810-6268 13 | keywords: 14 | - ruby 15 | repository-code: https://github.com/bkuhlmann/hemo 16 | repository-artifact: https://github.com/bkuhlmann/hemo 17 | url: https://github.com/bkuhlmann/hemo 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.4 2 | 3 | ARG RUBY_VERSION=3.3.4 4 | 5 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base 6 | 7 | LABEL description="Alchemists Application" 8 | LABEL maintainer="Brooke Kuhlmann " 9 | 10 | WORKDIR /app 11 | 12 | RUN < 1.8" 8 | gem "dry-schema", "~> 1.13" 9 | gem "dry-types", "~> 1.7" 10 | gem "dry-validation", "~> 1.10" 11 | gem "hanami", "~> 2.2.0" 12 | gem "hanami-assets", "~> 2.2.0" 13 | gem "hanami-cli", "~> 2.2.0" 14 | gem "hanami-controller", "~> 2.2.0" 15 | gem "hanami-db", "~> 2.2.0" 16 | gem "hanami-router", "~> 2.2.0" 17 | gem "hanami-validations", "~> 2.2.0" 18 | gem "hanami-view", "~> 2.2.0" 19 | gem "htmx", "~> 2.0" 20 | gem "pg", "~> 1.5" 21 | gem "puma", "~> 6.6" 22 | gem "rack-attack", "~> 6.7" 23 | gem "refinements", "~> 13.0" 24 | gem "rom", "~> 5.3" 25 | gem "rom-sql", "~> 3.6" 26 | gem "sequel", "~> 5.77" 27 | 28 | group :quality do 29 | gem "caliber", "~> 0.79" 30 | gem "git-lint", "~> 9.0" 31 | gem "reek", "~> 6.5", require: false 32 | gem "rubocop-sequel", "~> 0.4" 33 | gem "simplecov", "~> 0.22", require: false 34 | end 35 | 36 | group :development, :test do 37 | gem "dotenv", "~> 3.0" 38 | end 39 | 40 | group :development do 41 | gem "hanami-webconsole", "~> 2.2.0" 42 | gem "localhost", "~> 1.2" 43 | gem "rake", "~> 13.2" 44 | gem "rerun", "~> 0.14" 45 | end 46 | 47 | group :test do 48 | gem "capybara", "~> 3.40" 49 | gem "cuprite", "~> 0.15" 50 | gem "database_cleaner-sequel", "~> 2.0" 51 | gem "launchy", "~> 3.0" 52 | gem "rack-test", "~> 2.1" 53 | gem "rom-factory", "~> 0.12" 54 | gem "rspec", "~> 3.13" 55 | end 56 | 57 | group :tools do 58 | gem "amazing_print", "~> 1.8" 59 | gem "debug", "~> 1.10" 60 | gem "irb-kit", "~> 1.1" 61 | gem "repl_type_completor", "~> 0.1" 62 | end 63 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.7) 5 | public_suffix (>= 2.0.2, < 7.0) 6 | amazing_print (1.8.1) 7 | ast (2.4.3) 8 | base64 (0.2.0) 9 | better_errors (2.10.1) 10 | erubi (>= 1.0.0) 11 | rack (>= 0.9.0) 12 | rouge (>= 1.0.0) 13 | bigdecimal (3.1.9) 14 | binding_of_caller (1.0.1) 15 | debug_inspector (>= 1.2.0) 16 | caliber (0.80.2) 17 | rubocop (~> 1.75) 18 | rubocop-capybara (~> 2.22) 19 | rubocop-disable_syntax (~> 0.2) 20 | rubocop-packaging (~> 0.6) 21 | rubocop-performance (~> 1.25) 22 | rubocop-rake (~> 0.7) 23 | rubocop-rspec (~> 3.6) 24 | rubocop-thread_safety (~> 0.7) 25 | capybara (3.40.0) 26 | addressable 27 | matrix 28 | mini_mime (>= 0.1.3) 29 | nokogiri (~> 1.11) 30 | rack (>= 1.6.0) 31 | rack-test (>= 0.6.3) 32 | regexp_parser (>= 1.5, < 3.0) 33 | xpath (~> 3.2) 34 | childprocess (5.1.0) 35 | logger (~> 1.5) 36 | cogger (1.2.2) 37 | core (~> 2.0) 38 | logger (~> 1.7) 39 | refinements (~> 13.0) 40 | tone (~> 2.0) 41 | zeitwerk (~> 2.7) 42 | concurrent-ruby (1.3.5) 43 | containable (1.2.0) 44 | concurrent-ruby (~> 1.3) 45 | core (2.2.0) 46 | cuprite (0.17) 47 | capybara (~> 3.0) 48 | ferrum (~> 0.17.0) 49 | database_cleaner-core (2.0.1) 50 | database_cleaner-sequel (2.0.2) 51 | database_cleaner-core (~> 2.0.0) 52 | sequel 53 | date (3.4.1) 54 | debug (1.10.0) 55 | irb (~> 1.10) 56 | reline (>= 0.3.8) 57 | debug_inspector (1.2.0) 58 | diff-lcs (1.6.2) 59 | docile (1.4.1) 60 | dotenv (3.1.8) 61 | dry-auto_inject (1.1.0) 62 | dry-core (~> 1.1) 63 | zeitwerk (~> 2.6) 64 | dry-cli (1.2.0) 65 | dry-configurable (1.3.0) 66 | dry-core (~> 1.1) 67 | zeitwerk (~> 2.6) 68 | dry-core (1.1.0) 69 | concurrent-ruby (~> 1.0) 70 | logger 71 | zeitwerk (~> 2.6) 72 | dry-events (1.1.0) 73 | concurrent-ruby (~> 1.0) 74 | dry-core (~> 1.1) 75 | dry-files (1.1.0) 76 | dry-inflector (1.2.0) 77 | dry-initializer (3.2.0) 78 | dry-logger (1.1.0) 79 | dry-logic (1.6.0) 80 | bigdecimal 81 | concurrent-ruby (~> 1.0) 82 | dry-core (~> 1.1) 83 | zeitwerk (~> 2.6) 84 | dry-monads (1.8.3) 85 | concurrent-ruby (~> 1.0) 86 | dry-core (~> 1.1) 87 | zeitwerk (~> 2.6) 88 | dry-monitor (1.0.1) 89 | dry-configurable (~> 1.0, < 2) 90 | dry-core (~> 1.0, < 2) 91 | dry-events (~> 1.0, < 2) 92 | dry-schema (1.14.1) 93 | concurrent-ruby (~> 1.0) 94 | dry-configurable (~> 1.0, >= 1.0.1) 95 | dry-core (~> 1.1) 96 | dry-initializer (~> 3.2) 97 | dry-logic (~> 1.5) 98 | dry-types (~> 1.8) 99 | zeitwerk (~> 2.6) 100 | dry-struct (1.8.0) 101 | dry-core (~> 1.1) 102 | dry-types (~> 1.8, >= 1.8.2) 103 | ice_nine (~> 0.11) 104 | zeitwerk (~> 2.6) 105 | dry-system (1.2.2) 106 | dry-auto_inject (~> 1.1) 107 | dry-configurable (~> 1.3) 108 | dry-core (~> 1.1) 109 | dry-inflector (~> 1.1) 110 | dry-transformer (1.0.1) 111 | zeitwerk (~> 2.6) 112 | dry-types (1.8.2) 113 | bigdecimal (~> 3.0) 114 | concurrent-ruby (~> 1.0) 115 | dry-core (~> 1.0) 116 | dry-inflector (~> 1.0) 117 | dry-logic (~> 1.4) 118 | zeitwerk (~> 2.6) 119 | dry-validation (1.11.1) 120 | concurrent-ruby (~> 1.0) 121 | dry-core (~> 1.1) 122 | dry-initializer (~> 3.2) 123 | dry-schema (~> 1.14) 124 | zeitwerk (~> 2.6) 125 | erb (5.0.1) 126 | erubi (1.13.1) 127 | etcher (3.2.1) 128 | cogger (~> 1.0) 129 | core (~> 2.0) 130 | dry-monads (~> 1.8) 131 | dry-types (~> 1.7) 132 | refinements (~> 13.0) 133 | versionaire (~> 14.0) 134 | zeitwerk (~> 2.7) 135 | faker (3.5.1) 136 | i18n (>= 1.8.11, < 2) 137 | ferrum (0.17.1) 138 | addressable (~> 2.5) 139 | base64 (~> 0.2) 140 | concurrent-ruby (~> 1.1) 141 | webrick (~> 1.7) 142 | websocket-driver (~> 0.7) 143 | ffi (1.17.2-aarch64-linux-gnu) 144 | ffi (1.17.2-aarch64-linux-musl) 145 | ffi (1.17.2-arm-linux-gnu) 146 | ffi (1.17.2-arm-linux-musl) 147 | ffi (1.17.2-arm64-darwin) 148 | ffi (1.17.2-x86_64-darwin) 149 | ffi (1.17.2-x86_64-linux-gnu) 150 | ffi (1.17.2-x86_64-linux-musl) 151 | git-lint (9.3.1) 152 | cogger (~> 1.0) 153 | containable (~> 1.1) 154 | core (~> 2.0) 155 | dry-monads (~> 1.8) 156 | dry-schema (~> 1.13) 157 | etcher (~> 3.0) 158 | gitt (~> 4.1) 159 | infusible (~> 4.0) 160 | refinements (~> 13.0) 161 | runcom (~> 12.0) 162 | sod (~> 1.0) 163 | spek (~> 4.0) 164 | tone (~> 2.0) 165 | zeitwerk (~> 2.7) 166 | gitt (4.3.1) 167 | core (~> 2.0) 168 | dry-monads (~> 1.8) 169 | refinements (~> 13.0) 170 | zeitwerk (~> 2.7) 171 | hanami (2.2.1) 172 | bundler (>= 1.16, < 3) 173 | dry-configurable (~> 1.0, >= 1.2.0, < 2) 174 | dry-core (~> 1.0, < 2) 175 | dry-inflector (~> 1.0, >= 1.1.0, < 2) 176 | dry-logger (~> 1.0, < 2) 177 | dry-monitor (~> 1.0, >= 1.0.1, < 2) 178 | dry-system (~> 1.1) 179 | hanami-cli (~> 2.2.1) 180 | hanami-utils (~> 2.2) 181 | json (>= 2.7.2) 182 | zeitwerk (~> 2.6) 183 | hanami-assets (2.2.0) 184 | zeitwerk (~> 2.6) 185 | hanami-cli (2.2.1) 186 | bundler (~> 2.1) 187 | dry-cli (~> 1.0, >= 1.1.0) 188 | dry-files (~> 1.0, >= 1.0.2, < 2) 189 | dry-inflector (~> 1.0, < 2) 190 | rake (~> 13.0) 191 | zeitwerk (~> 2.6) 192 | hanami-controller (2.2.0) 193 | dry-configurable (~> 1.0, < 2) 194 | dry-core (~> 1.0) 195 | hanami-utils (~> 2.2) 196 | rack (~> 2.0) 197 | zeitwerk (~> 2.6) 198 | hanami-db (2.2.1) 199 | rom (~> 5.4, >= 5.4.1) 200 | rom-sql (~> 3.7) 201 | zeitwerk (~> 2.6) 202 | hanami-router (2.2.0) 203 | mustermann (~> 3.0) 204 | mustermann-contrib (~> 3.0) 205 | rack (~> 2.0) 206 | hanami-utils (2.2.0) 207 | concurrent-ruby (~> 1.0) 208 | dry-core (~> 1.0, < 2) 209 | dry-transformer (~> 1.0, < 2) 210 | hanami-validations (2.2.0) 211 | dry-validation (>= 1.10, < 2) 212 | hanami-view (2.2.1) 213 | dry-configurable (~> 1.0) 214 | dry-core (~> 1.0) 215 | dry-inflector (~> 1.0, < 2) 216 | temple (~> 0.10.0, >= 0.10.2) 217 | tilt (~> 2.3) 218 | zeitwerk (~> 2.6) 219 | hanami-webconsole (2.2.0) 220 | better_errors (~> 2.10, >= 2.10.1) 221 | binding_of_caller (~> 1.0) 222 | hansi (0.2.1) 223 | htmx (2.2.1) 224 | refinements (~> 13.0) 225 | zeitwerk (~> 2.7) 226 | i18n (1.14.7) 227 | concurrent-ruby (~> 1.0) 228 | ice_nine (0.11.2) 229 | infusible (4.3.0) 230 | marameters (~> 4.1) 231 | io-console (0.8.0) 232 | irb (1.15.2) 233 | pp (>= 0.6.0) 234 | rdoc (>= 4.0.0) 235 | reline (>= 0.4.2) 236 | irb-kit (1.2.1) 237 | irb (~> 1.15) 238 | zeitwerk (~> 2.7) 239 | json (2.12.2) 240 | language_server-protocol (3.17.0.5) 241 | launchy (3.1.1) 242 | addressable (~> 2.8) 243 | childprocess (~> 5.0) 244 | logger (~> 1.6) 245 | lint_roller (1.1.0) 246 | listen (3.9.0) 247 | rb-fsevent (~> 0.10, >= 0.10.3) 248 | rb-inotify (~> 0.9, >= 0.9.10) 249 | localhost (1.5.0) 250 | logger (1.7.0) 251 | marameters (4.3.1) 252 | refinements (~> 13.0) 253 | zeitwerk (~> 2.7) 254 | matrix (0.4.2) 255 | mini_mime (1.1.5) 256 | mustermann (3.0.3) 257 | ruby2_keywords (~> 0.0.1) 258 | mustermann-contrib (3.0.3) 259 | hansi (~> 0.2.0) 260 | mustermann (= 3.0.3) 261 | nio4r (2.7.4) 262 | nokogiri (1.18.8-aarch64-linux-gnu) 263 | racc (~> 1.4) 264 | nokogiri (1.18.8-aarch64-linux-musl) 265 | racc (~> 1.4) 266 | nokogiri (1.18.8-arm-linux-gnu) 267 | racc (~> 1.4) 268 | nokogiri (1.18.8-arm-linux-musl) 269 | racc (~> 1.4) 270 | nokogiri (1.18.8-arm64-darwin) 271 | racc (~> 1.4) 272 | nokogiri (1.18.8-x86_64-darwin) 273 | racc (~> 1.4) 274 | nokogiri (1.18.8-x86_64-linux-gnu) 275 | racc (~> 1.4) 276 | nokogiri (1.18.8-x86_64-linux-musl) 277 | racc (~> 1.4) 278 | optparse (0.6.0) 279 | parallel (1.27.0) 280 | parser (3.3.8.0) 281 | ast (~> 2.4.1) 282 | racc 283 | pg (1.5.9) 284 | pp (0.6.2) 285 | prettyprint 286 | prettyprint (0.2.0) 287 | prism (1.4.0) 288 | psych (5.2.6) 289 | date 290 | stringio 291 | public_suffix (6.0.2) 292 | puma (6.6.0) 293 | nio4r (~> 2.0) 294 | racc (1.8.1) 295 | rack (2.2.16) 296 | rack-attack (6.7.0) 297 | rack (>= 1.0, < 4) 298 | rack-test (2.2.0) 299 | rack (>= 1.3) 300 | rainbow (3.1.1) 301 | rake (13.2.1) 302 | rb-fsevent (0.11.2) 303 | rb-inotify (0.11.1) 304 | ffi (~> 1.0) 305 | rbs (3.9.4) 306 | logger 307 | rdoc (6.14.0) 308 | erb 309 | psych (>= 4.0.0) 310 | reek (6.5.0) 311 | dry-schema (~> 1.13) 312 | logger (~> 1.6) 313 | parser (~> 3.3.0) 314 | rainbow (>= 2.0, < 4.0) 315 | rexml (~> 3.1) 316 | refinements (13.2.0) 317 | regexp_parser (2.10.0) 318 | reline (0.6.1) 319 | io-console (~> 0.5) 320 | repl_type_completor (0.1.11) 321 | prism (~> 1.0) 322 | rbs (>= 2.7.0, < 4.0.0) 323 | rerun (0.14.0) 324 | listen (~> 3.0) 325 | rexml (3.4.1) 326 | rom (5.4.2) 327 | rom-changeset (~> 5.4) 328 | rom-core (~> 5.4) 329 | rom-repository (~> 5.4, >= 5.4.2) 330 | rom-changeset (5.4.0) 331 | dry-core (~> 1.0) 332 | rom-core (~> 5.4) 333 | transproc (~> 1.1) 334 | rom-core (5.4.0) 335 | concurrent-ruby (~> 1.1) 336 | dry-configurable (~> 1.0) 337 | dry-core (~> 1.0) 338 | dry-inflector (~> 1.0) 339 | dry-initializer (~> 3.2) 340 | dry-struct (~> 1.0) 341 | dry-types (~> 1.6) 342 | transproc (~> 1.1) 343 | rom-factory (0.13.0) 344 | dry-configurable (~> 1.3) 345 | dry-core (~> 1.1) 346 | dry-struct (~> 1.7) 347 | faker (>= 2.0, < 4) 348 | rom-core (~> 5.4) 349 | rom-repository (5.4.2) 350 | dry-core (~> 1.0) 351 | dry-initializer (~> 3.2) 352 | rom-core (~> 5.4) 353 | rom-sql (3.7.0) 354 | dry-core (~> 1.1) 355 | dry-types (~> 1.8) 356 | rom (~> 5.4) 357 | sequel (>= 4.49) 358 | rouge (4.5.2) 359 | rspec (3.13.0) 360 | rspec-core (~> 3.13.0) 361 | rspec-expectations (~> 3.13.0) 362 | rspec-mocks (~> 3.13.0) 363 | rspec-core (3.13.3) 364 | rspec-support (~> 3.13.0) 365 | rspec-expectations (3.13.4) 366 | diff-lcs (>= 1.2.0, < 2.0) 367 | rspec-support (~> 3.13.0) 368 | rspec-mocks (3.13.4) 369 | diff-lcs (>= 1.2.0, < 2.0) 370 | rspec-support (~> 3.13.0) 371 | rspec-support (3.13.3) 372 | rubocop (1.75.7) 373 | json (~> 2.3) 374 | language_server-protocol (~> 3.17.0.2) 375 | lint_roller (~> 1.1.0) 376 | parallel (~> 1.10) 377 | parser (>= 3.3.0.2) 378 | rainbow (>= 2.2.2, < 4.0) 379 | regexp_parser (>= 2.9.3, < 3.0) 380 | rubocop-ast (>= 1.44.0, < 2.0) 381 | ruby-progressbar (~> 1.7) 382 | unicode-display_width (>= 2.4.0, < 4.0) 383 | rubocop-ast (1.44.1) 384 | parser (>= 3.3.7.2) 385 | prism (~> 1.4) 386 | rubocop-capybara (2.22.1) 387 | lint_roller (~> 1.1) 388 | rubocop (~> 1.72, >= 1.72.1) 389 | rubocop-disable_syntax (0.2.0) 390 | lint_roller 391 | rubocop (>= 1.72.0) 392 | rubocop-packaging (0.6.0) 393 | lint_roller (~> 1.1.0) 394 | rubocop (>= 1.72.1, < 2.0) 395 | rubocop-performance (1.25.0) 396 | lint_roller (~> 1.1) 397 | rubocop (>= 1.75.0, < 2.0) 398 | rubocop-ast (>= 1.38.0, < 2.0) 399 | rubocop-rake (0.7.1) 400 | lint_roller (~> 1.1) 401 | rubocop (>= 1.72.1) 402 | rubocop-rspec (3.6.0) 403 | lint_roller (~> 1.1) 404 | rubocop (~> 1.72, >= 1.72.1) 405 | rubocop-sequel (0.4.1) 406 | lint_roller (~> 1.1) 407 | rubocop (>= 1.72.1, < 2) 408 | rubocop-thread_safety (0.7.2) 409 | lint_roller (~> 1.1) 410 | rubocop (~> 1.72, >= 1.72.1) 411 | ruby-progressbar (1.13.0) 412 | ruby2_keywords (0.0.5) 413 | runcom (12.2.1) 414 | refinements (~> 13.0) 415 | xdg (~> 9.0) 416 | zeitwerk (~> 2.7) 417 | sequel (5.92.0) 418 | bigdecimal 419 | simplecov (0.22.0) 420 | docile (~> 1.1) 421 | simplecov-html (~> 0.11) 422 | simplecov_json_formatter (~> 0.1) 423 | simplecov-html (0.13.1) 424 | simplecov_json_formatter (0.1.4) 425 | sod (1.2.1) 426 | cogger (~> 1.0) 427 | containable (~> 1.1) 428 | infusible (~> 4.0) 429 | optparse (~> 0.6) 430 | refinements (~> 13.0) 431 | tone (~> 2.0) 432 | zeitwerk (~> 2.7) 433 | spek (4.2.1) 434 | core (~> 2.0) 435 | dry-monads (~> 1.8) 436 | refinements (~> 13.0) 437 | versionaire (~> 14.0) 438 | zeitwerk (~> 2.7) 439 | stringio (3.1.7) 440 | temple (0.10.3) 441 | tilt (2.6.0) 442 | tone (2.3.1) 443 | refinements (~> 13.0) 444 | zeitwerk (~> 2.7) 445 | transproc (1.1.1) 446 | unicode-display_width (3.1.4) 447 | unicode-emoji (~> 4.0, >= 4.0.4) 448 | unicode-emoji (4.0.4) 449 | versionaire (14.2.0) 450 | refinements (~> 13.0) 451 | webrick (1.9.1) 452 | websocket-driver (0.8.0) 453 | base64 454 | websocket-extensions (>= 0.1.0) 455 | websocket-extensions (0.1.5) 456 | xdg (9.2.0) 457 | xpath (3.2.0) 458 | nokogiri (~> 1.8) 459 | zeitwerk (2.7.3) 460 | 461 | PLATFORMS 462 | aarch64-linux-gnu 463 | aarch64-linux-musl 464 | arm-linux-gnu 465 | arm-linux-musl 466 | arm64-darwin 467 | x86_64-darwin 468 | x86_64-linux-gnu 469 | x86_64-linux-musl 470 | 471 | DEPENDENCIES 472 | amazing_print (~> 1.8) 473 | caliber (~> 0.79) 474 | capybara (~> 3.40) 475 | cuprite (~> 0.15) 476 | database_cleaner-sequel (~> 2.0) 477 | debug (~> 1.10) 478 | dotenv (~> 3.0) 479 | dry-monads (~> 1.8) 480 | dry-schema (~> 1.13) 481 | dry-types (~> 1.7) 482 | dry-validation (~> 1.10) 483 | git-lint (~> 9.0) 484 | hanami (~> 2.2.0) 485 | hanami-assets (~> 2.2.0) 486 | hanami-cli (~> 2.2.0) 487 | hanami-controller (~> 2.2.0) 488 | hanami-db (~> 2.2.0) 489 | hanami-router (~> 2.2.0) 490 | hanami-validations (~> 2.2.0) 491 | hanami-view (~> 2.2.0) 492 | hanami-webconsole (~> 2.2.0) 493 | htmx (~> 2.0) 494 | irb-kit (~> 1.1) 495 | launchy (~> 3.0) 496 | localhost (~> 1.2) 497 | pg (~> 1.5) 498 | puma (~> 6.6) 499 | rack-attack (~> 6.7) 500 | rack-test (~> 2.1) 501 | rake (~> 13.2) 502 | reek (~> 6.5) 503 | refinements (~> 13.0) 504 | repl_type_completor (~> 0.1) 505 | rerun (~> 0.14) 506 | rom (~> 5.3) 507 | rom-factory (~> 0.12) 508 | rom-sql (~> 3.6) 509 | rspec (~> 3.13) 510 | rubocop-sequel (~> 0.4) 511 | sequel (~> 5.77) 512 | simplecov (~> 0.22) 513 | 514 | CHECKSUMS 515 | addressable (2.8.7) sha256=462986537cf3735ab5f3c0f557f14155d778f4b43ea4f485a9deb9c8f7c58232 516 | amazing_print (1.8.1) sha256=f53b4e1881f53f9663cb222840c7a027d0e61d46cc908366965739559c0d6d68 517 | ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 518 | base64 (0.2.0) sha256=0f25e9b21a02a0cc0cea8ef92b2041035d39350946e8789c562b2d1a3da01507 519 | better_errors (2.10.1) sha256=f798f1bac93f3e775925b7fcb24cffbcf0bb62ee2210f5350f161a6b75fc0a73 520 | bigdecimal (3.1.9) sha256=2ffc742031521ad69c2dfc815a98e426a230a3d22aeac1995826a75dabfad8cc 521 | binding_of_caller (1.0.1) sha256=2b2902abff4246ddcfbc4da9b69bc4a019e22aeb300c2ff6289a173d4b90b29a 522 | caliber (0.80.2) sha256=58e59d903e8467344db638d988cec08a1cfac29262475c63d9af2521bc8d762f 523 | capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef 524 | childprocess (5.1.0) sha256=9a8d484be2fd4096a0e90a0cd3e449a05bc3aa33f8ac9e4d6dcef6ac1455b6ec 525 | cogger (1.2.2) sha256=53a7c755cedd4cdc15f676fa82b1c5daabef386659b3c23a386e0ebe48a9b20b 526 | concurrent-ruby (1.3.5) sha256=813b3e37aca6df2a21a3b9f1d497f8cbab24a2b94cab325bffe65ee0f6cbebc6 527 | containable (1.2.0) sha256=c7c5209dd375d914ae68129388a6e019d1f1b6ab5a4278e6eaaaae29c3a01b01 528 | core (2.2.0) sha256=f396aa51af0ba84b9ccb77e07cd54a3c3154bbed95b0defe743e9f03dab5e2e3 529 | cuprite (0.17) sha256=b140d5dc70d08b97ad54bcf45cd95d0bd430e291e9dffe76fff851fddd57c12b 530 | database_cleaner-core (2.0.1) sha256=8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c 531 | database_cleaner-sequel (2.0.2) sha256=f1e6566a1a34d7425ff94b447e2c16181bc215555aff3dfe463b7977b621d4bf 532 | date (3.4.1) sha256=bf268e14ef7158009bfeaec40b5fa3c7271906e88b196d958a89d4b408abe64f 533 | debug (1.10.0) sha256=11e28ca74875979e612444104f3972bd5ffb9e79179907d7ad46dba44bd2e7a4 534 | debug_inspector (1.2.0) sha256=9bdfa02eebc3da163833e6a89b154084232f5766087e59573b70521c77ea68a2 535 | diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 536 | docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e 537 | dotenv (3.1.8) sha256=9e1176060ced581f8e6ce4384e91361817763a76e3c625c8bddc18b35bd392c3 538 | dry-auto_inject (1.1.0) sha256=f9276cb5d15a3ef138e1f1149e289e287f636de57ef4a6decd233542eb708f78 539 | dry-cli (1.2.0) sha256=8c97df3b290ee6354ee2025a5f0a67226b381627de32741d06f35ea0887cd95b 540 | dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8 541 | dry-core (1.1.0) sha256=0903821a9707649a7da545a2cd88e20f3a663ab1c5288abd7f914fa7751ab195 542 | dry-events (1.1.0) sha256=53f13960daf02cc06c2ea6cdf3209e29b0dd400f8977a8020834a68c044a78de 543 | dry-files (1.1.0) sha256=4fe91d007ad78d303b7f3a8a2321bbfc554fcc01aa11a6b37246658bbf8d41de 544 | dry-inflector (1.2.0) sha256=22f5d0b50fd57074ae57e2ca17e3b300e57564c218269dcf82ff3e42d3f38f2e 545 | dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3 546 | dry-logger (1.1.0) sha256=d99f34aa87b17495be9cf3af3bd44a09ae2db2640d227915a679f8c9293c88b5 547 | dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2 548 | dry-monads (1.8.3) sha256=5fbc06ae4ff76ae081922a902be998673703304d10b46b08931696f2c8decc06 549 | dry-monitor (1.0.1) sha256=4ac8ed1f85b14bfde1aa0bce9496d7d2fc699c5a4a1131010c8014d5f9e7a309 550 | dry-schema (1.14.1) sha256=2fcd7539a7099cacae6a22f6a3a2c1846fe5afeb1c841cde432c89c6cb9b9ff1 551 | dry-struct (1.8.0) sha256=74c38b559924fb6462ac43ec780c4533a082d7b1d238a8d7857b773b3b8e2966 552 | dry-system (1.2.2) sha256=cb346c1ad79de6d5f281a4d7893ce186c0b1b26c13a1330539b705289cc47e79 553 | dry-transformer (1.0.1) sha256=6a2a6184ece8e347ebb1315fe3224ac19f42ca157db0f59cd5a69aeb76f73d76 554 | dry-types (1.8.2) sha256=c84e9ada69419c727c3b12e191e0ed7d2c6d58d040d55e79ea16e0ebf8b3ec0f 555 | dry-validation (1.11.1) sha256=70900bb5a2d911c8aab566d3e360c6bff389b8bf92ea8e04885ce51c41ff8085 556 | erb (5.0.1) sha256=760439803b36cc93eca8a266aab614614e588024a89bc30a62e78d98ff452c23 557 | erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 558 | etcher (3.2.1) sha256=00cdec597e159133bb597c0007d26f3a3644778da450a278bbc45f04a8fd9307 559 | faker (3.5.1) sha256=1ad1fbea279d882f486059c23fe3ddb816ccd1d7052c05a45014b4450d859bfc 560 | ferrum (0.17.1) sha256=51d591120fc593e5a13b5d9d6474389f5145bb92a91e36eab147b5d096c8cbe7 561 | ffi (1.17.2-aarch64-linux-gnu) sha256=c910bd3cae70b76690418cce4572b7f6c208d271f323d692a067d59116211a1a 562 | ffi (1.17.2-aarch64-linux-musl) sha256=69e6556b091d45df83e6c3b19d3c54177c206910965155a6ec98de5e893c7b7c 563 | ffi (1.17.2-arm-linux-gnu) sha256=d4a438f2b40224ae42ec72f293b3ebe0ba2159f7d1bd47f8417e6af2f68dbaa5 564 | ffi (1.17.2-arm-linux-musl) sha256=977dfb7f3a6381206dbda9bc441d9e1f9366bf189a634559c3b7c182c497aaa3 565 | ffi (1.17.2-arm64-darwin) sha256=54dd9789be1d30157782b8de42d8f887a3c3c345293b57ffb6b45b4d1165f813 566 | ffi (1.17.2-x86_64-darwin) sha256=981f2d4e32ea03712beb26e55e972797c2c5a7b0257955d8667ba58f2da6440e 567 | ffi (1.17.2-x86_64-linux-gnu) sha256=05d2026fc9dbb7cfd21a5934559f16293815b7ce0314846fee2ac8efbdb823ea 568 | ffi (1.17.2-x86_64-linux-musl) sha256=97c0eb3981414309285a64dc4d466bd149e981c279a56371ef811395d68cb95c 569 | git-lint (9.3.1) sha256=9f4ea54a540e0d44e8906bbfb80a4ea23c4ad410f610d2df8778b5863fd7d539 570 | gitt (4.3.1) sha256=57b7d9f446bd8e161db3d34ea6ce688a4e9a9e26bb250f4e9a3ddcfcca9cb0a0 571 | hanami (2.2.1) sha256=bef14f84916ad3a1244728df206bbd2ae6f307af945e3a2fd44cfe3a639c6cd5 572 | hanami-assets (2.2.0) sha256=a58a03faacd35f675633a5b693783bee1db4bcfcd0506c4c894f4fb7afeea7c1 573 | hanami-cli (2.2.1) sha256=028d1235d17516b3f5ea265a84578d6011391827c1cb1b647735596f666ba338 574 | hanami-controller (2.2.0) sha256=2811692ab9a80051da7878e524f781d14166436a3bcb207366ab2f4b21a5d8ea 575 | hanami-db (2.2.1) sha256=afe187dabd8be50267142c5710a275cb1f8a85e3021312b10f2e64e3f66c8bd9 576 | hanami-router (2.2.0) sha256=88534072b6efb5976b30608b70065bcdbaa602282600e37cb0cb5e9d2077c042 577 | hanami-utils (2.2.0) sha256=7884ed3ef637697487d7cb57045adec3325b9273c550a34b0d0f0332ccee5f5a 578 | hanami-validations (2.2.0) sha256=87957a131ddd7350ba4387afd831e64c37669cdf56c9c2107eb0f748ec3c78e6 579 | hanami-view (2.2.1) sha256=09179b2ce6f30e1b9e939d85f44cf11ca4803bad7a91788376586b0e31bb2560 580 | hanami-webconsole (2.2.0) sha256=486fba76c2664a63fcd30729d9c97a763264944dc120ff6b3ad1e29ad06e0931 581 | hansi (0.2.1) sha256=7e27dfd729f1692a3a7690fc0ef3af9d5e3cfc0285bb135cae7e56180e4805db 582 | htmx (2.2.1) sha256=abf98c0f94d9840f77968d9be377dfd46d638aca9e2666af437149ad272bc27e 583 | i18n (1.14.7) sha256=ceba573f8138ff2c0915427f1fc5bdf4aa3ab8ae88c8ce255eb3ecf0a11a5d0f 584 | ice_nine (0.11.2) sha256=5d506a7d2723d5592dc121b9928e4931742730131f22a1a37649df1c1e2e63db 585 | infusible (4.3.0) sha256=b7bd1e55affbe3b7278d3221a7bff068ce47ce784c29aa747fae0ac94e340416 586 | io-console (0.8.0) sha256=cd6a9facbc69871d69b2cb8b926fc6ea7ef06f06e505e81a64f14a470fddefa2 587 | irb (1.15.2) sha256=222f32952e278da34b58ffe45e8634bf4afc2dc7aa9da23fed67e581aa50fdba 588 | irb-kit (1.2.1) sha256=7249eba394599c2112b6c1a0a13f7385681fe6a39c11a839eee06928d13cf8cf 589 | json (2.12.2) sha256=ba94a48ad265605c8fa9a50a5892f3ba6a02661aa010f638211f3cb36f44abf4 590 | language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc 591 | launchy (3.1.1) sha256=72b847b5cc961589dde2c395af0108c86ff0119f42d4648d25b5440ebb10059e 592 | lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 593 | listen (3.9.0) sha256=db9e4424e0e5834480385197c139cb6b0ae0ef28cc13310cfd1ca78377d59c67 594 | localhost (1.5.0) sha256=ac48998ca1a4df75bf3c7038beb9d0baa5afb934bc4ed991d1b795f6a34feaf2 595 | logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 596 | marameters (4.3.1) sha256=c1c35f824749b6a3c5b83d4f03718fa95715356e4a35ff95d9c980c8b0a78d77 597 | matrix (0.4.2) sha256=71083ccbd67a14a43bfa78d3e4dc0f4b503b9cc18e5b4b1d686dc0f9ef7c4cc0 598 | mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef 599 | mustermann (3.0.3) sha256=d1f8e9ba2ddaed47150ddf81f6a7ea046826b64c672fbc92d83bce6b70657e88 600 | mustermann-contrib (3.0.3) sha256=858035bfe0425d3cbf2cc2ff5bc2cda23426c1a70530881054f0341e07699d34 601 | nio4r (2.7.4) sha256=d95dee68e0bb251b8ff90ac3423a511e3b784124e5db7ff5f4813a220ae73ca9 602 | nokogiri (1.18.8-aarch64-linux-gnu) sha256=36badd2eb281fca6214a5188e24a34399b15d89730639a068d12931e2adc210e 603 | nokogiri (1.18.8-aarch64-linux-musl) sha256=664e0f9a77a7122a66d6c03abba7641ca610769a4728db55ee1706a0838b78a2 604 | nokogiri (1.18.8-arm-linux-gnu) sha256=17de01ca3adf9f8e187883ed73c672344d3dbb3c260f88ffa1008e8dc255a28e 605 | nokogiri (1.18.8-arm-linux-musl) sha256=6e6d7e71fc39572bd613a82d528cf54392c3de1ba5ce974f05c832b8187a040b 606 | nokogiri (1.18.8-arm64-darwin) sha256=483b5b9fb33653f6f05cbe00d09ea315f268f0e707cfc809aa39b62993008212 607 | nokogiri (1.18.8-x86_64-darwin) sha256=024cdfe7d9ae3466bba6c06f348fb2a8395d9426b66a3c82f1961b907945cc0c 608 | nokogiri (1.18.8-x86_64-linux-gnu) sha256=4a747875db873d18a2985ee2c320a6070c4a414ad629da625fbc58d1a20e5ecc 609 | nokogiri (1.18.8-x86_64-linux-musl) sha256=ddd735fba49475a395b9ea793bb6474e3a3125b89960339604d08a5397de1165 610 | optparse (0.6.0) sha256=25e90469c1cd44048a89dc01c1dde9d5f0bdf717851055fb18237780779b068c 611 | parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 612 | parser (3.3.8.0) sha256=2476364142b307fa5a1b1ece44f260728be23858a9c71078e956131a75453c45 613 | pg (1.5.9) sha256=761efbdf73b66516f0c26fcbe6515dc7500c3f0aa1a1b853feae245433c64fdc 614 | pp (0.6.2) sha256=947ec3120c6f92195f8ee8aa25a7b2c5297bb106d83b41baa02983686577b6ff 615 | prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 616 | prism (1.4.0) sha256=dc0e3e00e93160213dc2a65519d9002a4a1e7b962db57d444cf1a71565bb703e 617 | psych (5.2.6) sha256=814328aa5dcb6d604d32126a20bc1cbcf05521a5b49dbb1a8b30a07e580f316e 618 | public_suffix (6.0.2) sha256=bfa7cd5108066f8c9602e0d6d4114999a5df5839a63149d3e8b0f9c1d3558394 619 | puma (6.6.0) sha256=f25c06873eb3d5de5f0a4ebc783acc81a4ccfe580c760cfe323497798018ad87 620 | racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f 621 | rack (2.2.16) sha256=8082752aa493a793baa89500afb3544ff92bd4dd5a55a969d1fd2df9506979c4 622 | rack-attack (6.7.0) sha256=3ca47e8f66cd33b2c96af53ea4754525cd928ed3fa8da10ee6dad0277791d77c 623 | rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 624 | rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a 625 | rake (13.2.1) sha256=46cb38dae65d7d74b6020a4ac9d48afed8eb8149c040eccf0523bec91907059d 626 | rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe 627 | rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e 628 | rbs (3.9.4) sha256=8e42c8f133fc2d94b65f62f34479546de1247b79892b57584f625b61e518a5d7 629 | rdoc (6.14.0) sha256=2c46de58d7129b8743fcf6d76e3db971bdc914150e15ac06b386549bd82ed7db 630 | reek (6.5.0) sha256=d26d3a492773b2bbc228888067a21afe33ac07954a17dbd64cdeae42c4c69be1 631 | refinements (13.2.0) sha256=5986c5bc237d5c8bb45208d8f935af0577a70110d29eea7657647eb8cfb2c95b 632 | regexp_parser (2.10.0) sha256=cb6f0ddde88772cd64bff1dbbf68df66d376043fe2e66a9ef77fcb1b0c548c61 633 | reline (0.6.1) sha256=1afcc9d7cb1029cdbe780d72f2f09251ce46d3780050f3ec39c3ccc6b60675fb 634 | repl_type_completor (0.1.11) sha256=d302df8892a7f5992ca97df6943b5e44bdc94479caa65e34f812a26b669b624c 635 | rerun (0.14.0) sha256=996a5af2b2b34dcb30146c6c728addab9113e07c119003370daa9bf27feb88fd 636 | rexml (3.4.1) sha256=c74527a9a0a04b4ec31dbe0dc4ed6004b960af943d8db42e539edde3a871abca 637 | rom (5.4.2) sha256=a8085e9018104f00b0af6ece68160a2241b60bdb45968bc7b104c79b80cdfd9d 638 | rom-changeset (5.4.0) sha256=503b9d2ad082743a28c06da70d431c7f12c18c4c0bca7f90c66d5aa2e13db898 639 | rom-core (5.4.0) sha256=a08264e40892cf86a57e32f6c09803b30e4fb3b1da86a64d41e71d92575df08f 640 | rom-factory (0.13.0) sha256=b6a3d45f79a2c48117ae1550a0790d1cd6b26b27fdff6307e7368ddb7a65ef85 641 | rom-repository (5.4.2) sha256=005433d29bd6f792b44099fa6b8844270202cb4d63b6a91cb136a86bc681b6d9 642 | rom-sql (3.7.0) sha256=56ba0714603d72d7ab5ad3247b6526d04ae83b6fcc807f4a1fa953cd7a65a639 643 | rouge (4.5.2) sha256=034233fb8a69d0ad0e0476943184e04cb971b68e3c2239724e02f428878b68a3 644 | rspec (3.13.0) sha256=d490914ac1d5a5a64a0e1400c1d54ddd2a501324d703b8cfe83f458337bab993 645 | rspec-core (3.13.3) sha256=25136507f4f9cf2e8977a2851e64e438b4331646054e345998714108745cdfe4 646 | rspec-expectations (3.13.4) sha256=4e43459765dfee900b25aa1361e106ab0799895ede65fc57872069feb559ecd8 647 | rspec-mocks (3.13.4) sha256=6bb158a0719c53d522104ed34c0777b884b2c9dc775ce64eaa10207df02ab993 648 | rspec-support (3.13.3) sha256=2a61e393f6e18b7228726e0c6869c5d5a1419d37206116c4d917d145276b3f43 649 | rubocop (1.75.7) sha256=23566ebb25263f26020687f8abb8aec049f3e29b6a00bdf0aa9d1db16b558be9 650 | rubocop-ast (1.44.1) sha256=e3cc04203b2ef04f6d6cf5f85fe6d643f442b18cc3b23e3ada0ce5b6521b8e92 651 | rubocop-capybara (2.22.1) sha256=ced88caef23efea53f46e098ff352f8fc1068c649606ca75cb74650970f51c0c 652 | rubocop-disable_syntax (0.2.0) sha256=1e61645773b3fc2f74e995ec65f605f7db59437c274fdfd4f385f60bf86af73e 653 | rubocop-packaging (0.6.0) sha256=fb92bd0fb48e6f8cdb1648d2249b0cd51c2497dcc87340132d22f01edbf558a7 654 | rubocop-performance (1.25.0) sha256=6f7d03568a770054117a78d0a8e191cefeffb703b382871ca7743831b1a52ec1 655 | rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d 656 | rubocop-rspec (3.6.0) sha256=c0e4205871776727e54dee9cc91af5fd74578001551ba40e1fe1a1ab4b404479 657 | rubocop-sequel (0.4.1) sha256=f325dc470c1e3191a616b41a4bf8cfbb2c3c2f4fbc2eee6286de019ca1f7c113 658 | rubocop-thread_safety (0.7.2) sha256=bd51449c420b1ddda5672b71a39706367402beb55aaf19fc020c1868717f31f6 659 | ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 660 | ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef 661 | runcom (12.2.1) sha256=2c99aee565ffb6fc476b8e33d8ec80bc88fd7373a617a9755be1744ad71cf1eb 662 | sequel (5.92.0) sha256=cb06a61945b8647e5fd93f20402acce16cac559d7757417a5854151f294c443d 663 | simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 664 | simplecov-html (0.13.1) sha256=5dab0b7ee612e60e9887ad57693832fdf4695b4c0c859eaea5f95c18791ef10b 665 | simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 666 | sod (1.2.1) sha256=b4697bbb6686eb07611b223f77fb2eea7be9a14b27ea10e7480649b70bf98f94 667 | spek (4.2.1) sha256=f617723e0a1ce22224bf9f541ccb8edfb656d4c6f66c8f0b89291306947d3116 668 | stringio (3.1.7) sha256=5b78b7cb242a315fb4fca61a8255d62ec438f58da2b90be66048546ade4507fa 669 | temple (0.10.3) sha256=df3145fe6577af1e25387eb7f7122d32ed51bdb6f2e7bb0f4fbf07b66151913b 670 | tilt (2.6.0) sha256=263d748466e0d83e510aa1a2e2281eff547937f0ef06be33d3632721e255f76b 671 | tone (2.3.1) sha256=24228edead3fd181add0d3988a2f2b3fb4a785ba83dbc9a89fbfe1ab98d84c42 672 | transproc (1.1.1) sha256=b2f9a85ecaf7064f0b3893ffac929de9fd713b13f5aaf822b0bd091fd6f5c8c2 673 | unicode-display_width (3.1.4) sha256=8caf2af1c0f2f07ec89ef9e18c7d88c2790e217c482bfc78aaa65eadd5415ac1 674 | unicode-emoji (4.0.4) sha256=2c2c4ef7f353e5809497126285a50b23056cc6e61b64433764a35eff6c36532a 675 | versionaire (14.2.0) sha256=f52fbcbc54d6b37f63a8151de7d3f1ec8cb0d2cc5a1886723497a8686a634476 676 | webrick (1.9.1) sha256=b42d3c94f166f3fb73d87e9b359def9b5836c426fc8beacf38f2184a21b2a989 677 | websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 678 | websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 679 | xdg (9.2.0) sha256=ad8bff38e0074a46aaa9d7322a01291b75e828d957cc08091dba3c35e931cb4d 680 | xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e 681 | zeitwerk (2.7.3) sha256=b2e86b4a9b57d26ba68a15230dcc7fe6f040f06831ce64417b0621ad96ba3e85 682 | 683 | RUBY VERSION 684 | ruby 3.4.4p34 685 | 686 | BUNDLED WITH 687 | 2.6.9 688 | -------------------------------------------------------------------------------- /LICENSE.adoc: -------------------------------------------------------------------------------- 1 | = Hippocratic License 2 | 3 | Version: 2.1.0. 4 | 5 | Purpose. The purpose of this License is for the Licensor named above to 6 | permit the Licensee (as defined below) broad permission, if consistent 7 | with Human Rights Laws and Human Rights Principles (as each is defined 8 | below), to use and work with the Software (as defined below) within the 9 | full scope of Licensor’s copyright and patent rights, if any, in the 10 | Software, while ensuring attribution and protecting the Licensor from 11 | liability. 12 | 13 | Permission and Conditions. The Licensor grants permission by this 14 | license ("License"), free of charge, to the extent of Licensor’s 15 | rights under applicable copyright and patent law, to any person or 16 | entity (the "Licensee") obtaining a copy of this software and 17 | associated documentation files (the "Software"), to do everything with 18 | the Software that would otherwise infringe (i) the Licensor’s copyright 19 | in the Software or (ii) any patent claims to the Software that the 20 | Licensor can license or becomes able to license, subject to all of the 21 | following terms and conditions: 22 | 23 | * Acceptance. This License is automatically offered to every person and 24 | entity subject to its terms and conditions. Licensee accepts this 25 | License and agrees to its terms and conditions by taking any action with 26 | the Software that, absent this License, would infringe any intellectual 27 | property right held by Licensor. 28 | * Notice. Licensee must ensure that everyone who gets a copy of any part 29 | of this Software from Licensee, with or without changes, also receives 30 | the License and the above copyright notice (and if included by the 31 | Licensor, patent, trademark and attribution notice). Licensee must cause 32 | any modified versions of the Software to carry prominent notices stating 33 | that Licensee changed the Software. For clarity, although Licensee is 34 | free to create modifications of the Software and distribute only the 35 | modified portion created by Licensee with additional or different terms, 36 | the portion of the Software not modified must be distributed pursuant to 37 | this License. If anyone notifies Licensee in writing that Licensee has 38 | not complied with this Notice section, Licensee can keep this License by 39 | taking all practical steps to comply within 30 days after the notice. If 40 | Licensee does not do so, Licensee’s License (and all rights licensed 41 | hereunder) shall end immediately. 42 | * Compliance with Human Rights Principles and Human Rights Laws. 43 | [arabic] 44 | . Human Rights Principles. 45 | [loweralpha] 46 | .. Licensee is advised to consult the articles of the United Nations 47 | Universal Declaration of Human Rights and the United Nations Global 48 | Compact that define recognized principles of international human rights 49 | (the "Human Rights Principles"). Licensee shall use the Software in a 50 | manner consistent with Human Rights Principles. 51 | .. Unless the Licensor and Licensee agree otherwise, any dispute, 52 | controversy, or claim arising out of or relating to (i) Section 1(a) 53 | regarding Human Rights Principles, including the breach of Section 1(a), 54 | termination of this License for breach of the Human Rights Principles, 55 | or invalidity of Section 1(a) or (ii) a determination of whether any Law 56 | is consistent or in conflict with Human Rights Principles pursuant to 57 | Section 2, below, shall be settled by arbitration in accordance with the 58 | Hague Rules on Business and Human Rights Arbitration (the "Rules"); 59 | provided, however, that Licensee may elect not to participate in such 60 | arbitration, in which event this License (and all rights licensed 61 | hereunder) shall end immediately. The number of arbitrators shall be one 62 | unless the Rules require otherwise. 63 | + 64 | Unless both the Licensor and Licensee agree to the contrary: (1) All 65 | documents and information concerning the arbitration shall be public and 66 | may be disclosed by any party; (2) The repository referred to under 67 | Article 43 of the Rules shall make available to the public in a timely 68 | manner all documents concerning the arbitration which are communicated 69 | to it, including all submissions of the parties, all evidence admitted 70 | into the record of the proceedings, all transcripts or other recordings 71 | of hearings and all orders, decisions and awards of the arbitral 72 | tribunal, subject only to the arbitral tribunal’s powers to take such 73 | measures as may be necessary to safeguard the integrity of the arbitral 74 | process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3) 75 | Article 26(6) of the Rules shall not apply. 76 | . Human Rights Laws. The Software shall not be used by any person or 77 | entity for any systems, activities, or other uses that violate any Human 78 | Rights Laws. "Human Rights Laws" means any applicable laws, 79 | regulations, or rules (collectively, "Laws") that protect human, 80 | civil, labor, privacy, political, environmental, security, economic, due 81 | process, or similar rights; provided, however, that such Laws are 82 | consistent and not in conflict with Human Rights Principles (a dispute 83 | over the consistency or a conflict between Laws and Human Rights 84 | Principles shall be determined by arbitration as stated above). Where 85 | the Human Rights Laws of more than one jurisdiction are applicable or in 86 | conflict with respect to the use of the Software, the Human Rights Laws 87 | that are most protective of the individuals or groups harmed shall 88 | apply. 89 | . Indemnity. Licensee shall hold harmless and indemnify Licensor (and 90 | any other contributor) against all losses, damages, liabilities, 91 | deficiencies, claims, actions, judgments, settlements, interest, awards, 92 | penalties, fines, costs, or expenses of whatever kind, including 93 | Licensor’s reasonable attorneys’ fees, arising out of or relating to 94 | Licensee’s use of the Software in violation of Human Rights Laws or 95 | Human Rights Principles. 96 | * Failure to Comply. Any failure of Licensee to act according to the 97 | terms and conditions of this License is both a breach of the License and 98 | an infringement of the intellectual property rights of the Licensor 99 | (subject to exceptions under Laws, e.g., fair use). In the event of a 100 | breach or infringement, the terms and conditions of this License may be 101 | enforced by Licensor under the Laws of any jurisdiction to which 102 | Licensee is subject. Licensee also agrees that the Licensor may enforce 103 | the terms and conditions of this License against Licensee through 104 | specific performance (or similar remedy under Laws) to the extent 105 | permitted by Laws. For clarity, except in the event of a breach of this 106 | License, infringement, or as otherwise stated in this License, Licensor 107 | may not terminate this License with Licensee. 108 | * Enforceability and Interpretation. If any term or provision of this 109 | License is determined to be invalid, illegal, or unenforceable by a 110 | court of competent jurisdiction, then such invalidity, illegality, or 111 | unenforceability shall not affect any other term or provision of this 112 | License or invalidate or render unenforceable such term or provision in 113 | any other jurisdiction; provided, however, subject to a court 114 | modification pursuant to the immediately following sentence, if any term 115 | or provision of this License pertaining to Human Rights Laws or Human 116 | Rights Principles is deemed invalid, illegal, or unenforceable against 117 | Licensee by a court of competent jurisdiction, all rights in the 118 | Software granted to Licensee shall be deemed null and void as between 119 | Licensor and Licensee. Upon a determination that any term or provision 120 | is invalid, illegal, or unenforceable, to the extent permitted by Laws, 121 | the court may modify this License to affect the original purpose that 122 | the Software be used in compliance with Human Rights Principles and 123 | Human Rights Laws as closely as possible. The language in this License 124 | shall be interpreted as to its fair meaning and not strictly for or 125 | against any party. 126 | * Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES 127 | "AS IS," WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND 128 | ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR 129 | OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE 130 | OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM. 131 | 132 | This Hippocratic License is an link:https://ethicalsource.dev[Ethical Source license] and is offered 133 | for use by licensors and licensees at their own risk, on an "AS IS" basis, and with no warranties 134 | express or implied, to the maximum extent permitted by Laws. 135 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | web: bundle exec puma --config ./config/puma.rb 4 | assets: bundle exec hanami assets compile 5 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :toc: macro 2 | :toclevels: 5 3 | :figure-caption!: 4 | 5 | :guard_link: link:https://github.com/guard/guard[Guard] 6 | :hanami_link: link:https://hanamirb.org[Hanami] 7 | :hanamismith_link: link:https://alchemists.io/projects/hanamismith[Hanamismith] 8 | :htmx_link: link:https://htmx.org[htmx] 9 | :overmind_link: link:https://github.com/DarthSim/overmind[Overmind] 10 | :postgres_link: link:https://www.postgresql.org[PostgreSQL] 11 | 12 | = Hemo 13 | 14 | image:https://dl.circleci.com/status-badge/img/gh/bkuhlmann/hemo/tree/main.svg?style=svg["CircleCI", link="https://dl.circleci.com/status-badge/redirect/gh/bkuhlmann/hemo/tree/main"] 15 | image:https://github.com/bkuhlmann/hemo/actions/workflows/ci.yml/badge.svg[GitHub Actions, link="https://github.com/bkuhlmann/hemo/actions"] 16 | 17 | Hemo is a portmanteau (i.e. `[H]anami + D[emo] = Hemo`) which is designed to provide a fully working demo {hanami_link} application as built by the {hanamismith_link} gem. 18 | 19 | ⚠️ This application is meant for _demonstration purposes only_ which means all commits are heavily link:https://alchemists.io/articles/git_rebase[rebased] as new functionality is implemented. You can _definitely_ clone this project -- and is encouraged -- but I wouldn't recommend forking this project because the SHAs will be constantly changing since each commit is meant to tell a story so people can learn how this application was architected. If you do clone (or download) a copy of this application, please note you'll have to re-clone/download with any new changes pushed to this repository. 20 | 21 | toc::[] 22 | 23 | == Features 24 | 25 | * Uses {hanamismith_link} for building the initial project skeleton and application architecture. 26 | * Uses modern {hanami_link} (backend) and {htmx_link} (frontend) technology to rapidly develop full featured web applications. 27 | * Uses modern CSS for stylesheets. 28 | * Provides a simple task management system for demonstration purposes where you can view, create, edit, update, and destroy tasks. 29 | 30 | == Screencasts 31 | 32 | See link:https://alchemists.io/projects/hanamismith/#_screenshotsscreencasts[Hanamismith] for details. 33 | 34 | == Requirements 35 | 36 | . link:https://www.ruby-lang.org[Ruby]. 37 | . {postgres_link}. 38 | . {overmind_link} (optional but recommended). 39 | 40 | == Setup 41 | 42 | To set up the project, run: 43 | 44 | [source,bash] 45 | ---- 46 | git clone https://github.com/bkuhlmann/hemo 47 | cd hemo 48 | bin/setup 49 | ---- 50 | 51 | == Usage 52 | 53 | For access to the console, run: 54 | 55 | [source,bash] 56 | ---- 57 | bin/console 58 | ---- 59 | 60 | To view all Rake tasks, run: 61 | 62 | [source,bash] 63 | ---- 64 | rake -T 65 | ---- 66 | 67 | To view all Hanami CLI or CLI subcommand options, run: 68 | 69 | [source,bash] 70 | ---- 71 | bin/hanami -h 72 | bin/hanami db -h 73 | ---- 74 | 75 | To develop -- red, green, refactor -- with {guard_link}, run: 76 | 77 | [source,bash] 78 | ---- 79 | bin/guard 80 | ---- 81 | 82 | To launch the server, use any of the following: 83 | 84 | [source,bash] 85 | ---- 86 | # With Overmind (recommended) 87 | overmind start --procfile Procfile.dev 88 | 89 | # Without Overmind 90 | bin/hanami server 91 | ---- 92 | 93 | Once the server is running, you can view the app via the following URLs: 94 | 95 | - `https://localhost:2443` (secure) 96 | - `http://localhost:2300` (insecure) 97 | 98 | You can also check the status (health) of the app by hitting the `/up` endpoint. 99 | 100 | == Tests 101 | 102 | To test, run: 103 | 104 | [source,bash] 105 | ---- 106 | bin/rake 107 | ---- 108 | 109 | == link:https://alchemists.io/policies/license[License] 110 | 111 | == link:https://alchemists.io/policies/security[Security] 112 | 113 | == link:https://alchemists.io/policies/code_of_conduct[Code of Conduct] 114 | 115 | == link:https://alchemists.io/policies/contributions[Contributions] 116 | 117 | == link:https://alchemists.io/policies/developer_certificate_of_origin[Developer Certificate of Origin] 118 | 119 | == link:https://alchemists.io/community[Community] 120 | 121 | == Credits 122 | 123 | * Built with {hanamismith_link}. 124 | * Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann]. 125 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "git/lint/rake/register" 5 | require "hanami/rake_tasks" 6 | require "reek/rake/task" 7 | require "rspec/core/rake_task" 8 | require "rubocop/rake_task" 9 | 10 | Git::Lint::Rake::Register.call 11 | Reek::Rake::Task.new 12 | RSpec::Core::RakeTask.new { |task| task.verbose = false } 13 | RuboCop::RakeTask.new 14 | 15 | desc "Run code quality checks" 16 | task quality: %i[git_lint reek rubocop] 17 | 18 | task default: %i[quality spec] 19 | -------------------------------------------------------------------------------- /app/action.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | require "hanami/action" 5 | 6 | module Hemo 7 | # The application base action. 8 | class Action < Hanami::Action 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/assets/css/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | .home-page { 6 | --color-pink: hsl(306, 81%, 77%); 7 | --color-red: hsl(11, 86%, 46%); 8 | --color-rose: hsl(338, 27%, 88%); 9 | --color-white: hsl(0, 0%, 100%); 10 | --font-family: Verdana; 11 | 12 | background-color: var(--color-white); 13 | background-image: linear-gradient( 14 | 0deg, 15 | hsl(351deg 100% 95%) 0%, 16 | hsl(352deg 100% 96%) 16%, 17 | hsl(352deg 100% 98%) 24%, 18 | hsl(352deg 100% 99%) 30%, 19 | hsl(300deg 100% 100%) 35%, 20 | hsl(300deg 100% 100%) 40%, 21 | hsl(300deg 100% 100%) 45%, 22 | hsl(0deg 0% 100%) 50%, 23 | hsl(300deg 100% 100%) 55%, 24 | hsl(300deg 100% 100%) 60%, 25 | hsl(300deg 100% 100%) 65%, 26 | hsl(300deg 100% 100%) 70%, 27 | hsl(300deg 100% 100%) 76%, 28 | hsl(300deg 100% 100%) 84%, 29 | hsl(0deg 0% 100%) 100% 30 | ); 31 | display: flex; 32 | flex-direction: column; 33 | font-family: var(--font-family); 34 | justify-content: space-between; 35 | min-height: 97vh; 36 | 37 | .navigation { 38 | align-items: center; 39 | display: flex; 40 | flex-direction: column; 41 | flex-wrap: wrap; 42 | font-size: 1.5rem; 43 | font-weight: 600; 44 | gap: 1rem; 45 | justify-content: center; 46 | list-style: none; 47 | padding: 0; 48 | } 49 | 50 | @media only screen and (min-width: 350px) { 51 | .navigation { 52 | flex-direction: row; 53 | justify-content: normal; 54 | margin: 1rem 2rem; 55 | } 56 | } 57 | 58 | .body { 59 | align-items: center; 60 | display: flex; 61 | flex-direction: column; 62 | 63 | .label { 64 | font-size: clamp(1.5rem, 2vw + 1rem, 2.25rem); 65 | } 66 | 67 | .logo { 68 | max-width: 100%; 69 | height: auto; 70 | object-fit: cover; 71 | } 72 | } 73 | 74 | .footer { 75 | display: flex; 76 | flex-direction: column; 77 | gap: 1rem; 78 | align-items: center; 79 | 80 | .group { 81 | display: flex; 82 | flex-wrap: wrap; 83 | gap: 1rem; 84 | justify-content: center; 85 | list-style: none; 86 | margin: 0; 87 | padding: 0; 88 | text-align: center; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/assets/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/assets/js/app.js: -------------------------------------------------------------------------------- 1 | import "../css/app.css"; 2 | -------------------------------------------------------------------------------- /app/assets/pwa/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hemo", 3 | "short_name": "Hemo", 4 | "description": "A Hanamismith skeleton application.", 5 | "icons": [ 6 | { 7 | "src": "https://alchemists.io/images/projects/hanamismith/icons/small.png", 8 | "type": "image/png", 9 | "sizes": "192x192" 10 | }, 11 | { 12 | "src": "https://alchemists.io/images/projects/hanamismith/icons/large.png", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | } 16 | ], 17 | "display": "standalone", 18 | "start_url": "/", 19 | "scope": "/", 20 | "theme_color": "#000000" 21 | } 22 | -------------------------------------------------------------------------------- /app/db/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami/db/relation" 4 | 5 | module Hemo 6 | module DB 7 | # The application database base relation. 8 | class Relation < Hanami::DB::Relation 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/db/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami/db/repo" 4 | 5 | module Hemo 6 | module DB 7 | # The application database base repository. 8 | class Repository < Hanami::DB::Repo 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/db/struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami/db/struct" 4 | 5 | module Hemo 6 | module DB 7 | # The application database base struct. 8 | class Struct < Hanami::DB::Struct 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/templates/layouts/app.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= content_for :title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | <%= tag.link title: "Hemo: Icon", 21 | rel: :icon, 22 | href: app_assets["icon.svg"], 23 | type: "image/svg+xml" %> 24 | 28 | <%= tag.link title: "Hemo: Manifest", 29 | rel: :manifest, 30 | href: app_assets["manifest.webmanifest"] %> 31 | 32 | <%= tag.link title: "Hemo: Stylesheet", rel: :stylesheet, href: app_assets["app.css"] %> 33 | 34 | 35 | 36 | <%= yield %> 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/view.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | require "hanami/view" 5 | 6 | module Hemo 7 | # The application base view. 8 | class View < Hanami::View 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | Bundler.require :tools 6 | 7 | require "hanami/prepare" 8 | require "irb" 9 | 10 | unless Hanami.env? :development, :test 11 | ENV["IRB_USE_AUTOCOMPLETE"] ||= "false" 12 | puts "IRB autocomplete disabled." 13 | end 14 | 15 | IRB.start __FILE__ 16 | -------------------------------------------------------------------------------- /bin/docker/build: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | Bundler.require :tools 6 | 7 | version = Bundler.root.join(".ruby-version").read.chop 8 | 9 | system <<~COMMAND 10 | docker buildx \ 11 | build \ 12 | --load \ 13 | --build-arg RUBY_VERSION=#{version} \ 14 | --tag hemo:latest \ 15 | #{Bundler.root} 16 | COMMAND 17 | -------------------------------------------------------------------------------- /bin/docker/console: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | Bundler.require :tools 6 | 7 | system <<~COMMAND 8 | docker run \ 9 | --disable-content-trust \ 10 | --pull never \ 11 | --interactive \ 12 | --tty \ 13 | --rm hemo:latest \ 14 | bash 15 | COMMAND 16 | -------------------------------------------------------------------------------- /bin/docker/entrypoint: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $VERBOSE = true 5 | 6 | path = Dir["/usr/lib/*/libjemalloc.so.2"] 7 | ENV["LD_PRELOAD"] = path.first unless ENV.key?("LD_PRELOAD") && path.empty? 8 | 9 | system ARGV.join(" ") 10 | -------------------------------------------------------------------------------- /bin/hanami: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "hanami-cli", "hanami" 7 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rake", "rake" 7 | -------------------------------------------------------------------------------- /bin/render/setup: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -o nounset 4 | set -o errexit 5 | set -o pipefail 6 | IFS=$'\n\t' 7 | 8 | bundle config set --local without "development,test,quality,tools" 9 | bundle install 10 | npm install 11 | bin/hanami db setup 12 | bin/hanami db seed 13 | bin/hanami assets compile 14 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rspec-core", "rspec" 7 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | load Gem.bin_path "rubocop", "rubocop" 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "debug" 5 | require "fileutils" 6 | require "pathname" 7 | 8 | APP_ROOT = Pathname(__dir__).join("..").expand_path 9 | 10 | Runner = lambda do |*arguments, kernel: Kernel| 11 | kernel.system(*arguments) || kernel.abort("\nERROR: Command #{arguments.inspect} failed.") 12 | end 13 | 14 | FileUtils.chdir APP_ROOT do 15 | puts "Installing gem dependencies..." 16 | Runner.call "bundle install" 17 | 18 | puts "Installing package dependencies..." 19 | Runner.call "npm install" 20 | 21 | puts "Creating and migrating databases..." 22 | Runner.call "hanami db prepare" 23 | 24 | puts "Compiling assets..." 25 | Runner.call "hanami assets compile" 26 | end 27 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami/boot" 4 | Bundler.require :tools if Hanami.env? :development 5 | 6 | run Hanami.app 7 | -------------------------------------------------------------------------------- /config/app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami" 4 | 5 | require_relative "initializers/rack_attack" 6 | 7 | module Hemo 8 | # Defines global application configuration. 9 | class App < Hanami::App 10 | RubyVM::YJIT.enable 11 | Dry::Schema.load_extensions :monads 12 | Dry::Validation.load_extensions :monads 13 | 14 | config.actions.content_security_policy.then do |csp| 15 | csp[:manifest_src] = "'self'" 16 | csp[:script_src] += " 'unsafe-eval' 'unsafe-inline' https://unpkg.com/" 17 | end 18 | 19 | config.middleware.use Rack::Attack 20 | config.middleware.use Rack::Deflater 21 | 22 | environment :development do 23 | # :nocov: 24 | config.logger.options[:colorize] = true 25 | 26 | config.logger = config.logger.instance.add_backend( 27 | colorize: false, 28 | stream: root.join("log/development.log") 29 | ) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/assets.js: -------------------------------------------------------------------------------- 1 | import * as assets from "hanami-assets"; 2 | 3 | await assets.run(); 4 | -------------------------------------------------------------------------------- /config/initializers/rack_attack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ipaddr" 4 | require "rack/attack" 5 | 6 | private_subnets = [ 7 | IPAddr.new("10.0.0.0/8"), 8 | IPAddr.new("172.16.0.0/12"), 9 | IPAddr.new("192.168.0.0/16"), 10 | IPAddr.new("127.0.0.1"), 11 | IPAddr.new("::1") 12 | ] 13 | 14 | Rack::Attack.safelist "allow private network" do |request| 15 | private_subnets.any? { |subnet| subnet.include? request.ip } 16 | end 17 | 18 | Rack::Attack.throttle("requests by IP", limit: 100, period: 60, &:ip) 19 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | development = ENV.fetch("HANAMI_ENV", "development") == "development" 4 | 5 | require "concurrent" 6 | require "localhost" if development 7 | 8 | Bundler.require :tools if development 9 | Bundler.root.join("tmp").then { |path| path.mkdir unless path.exist? } 10 | 11 | max_threads = ENV.fetch "HANAMI_MAX_THREADS", 5 12 | min_threads = ENV.fetch "HANAMI_MIN_THREADS", max_threads 13 | concurrency = ENV.fetch "HANAMI_WEB_CONCURRENCY", Concurrent.physical_processor_count 14 | 15 | threads min_threads, max_threads 16 | port ENV.fetch "HANAMI_PORT", 2300 17 | environment ENV.fetch "HANAMI_ENV", "development" 18 | workers concurrency 19 | worker_timeout 3600 if development 20 | ssl_bind "localhost", 2443 if development 21 | pidfile ENV.fetch "PIDFILE", "tmp/server.pid" 22 | plugin :tmp_restart 23 | 24 | preload_app! && before_fork { Hanami.shutdown } if concurrency.to_i.positive? 25 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hemo 4 | # Defines application routes. 5 | class Routes < Hanami::Routes 6 | slice(:health, at: "/up") { root to: "show" } 7 | slice(:home, at: "/") { root to: "show" } 8 | 9 | slice :tasks, at: "/tasks" do 10 | root to: "index" 11 | 12 | get "/:id", to: "show", as: :show 13 | get "/new", to: "new", as: :new 14 | post "/", to: "create", as: :create 15 | get "/:id/edit", to: "edit", as: :edit 16 | patch "/:id", to: "patch", as: :patch 17 | put "/:id", to: "update", as: :update 18 | delete "/:id", to: "delete", as: :delete 19 | end 20 | 21 | use Rack::Static, root: "public", urls: ["/.well-known/security.txt"] 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /config/settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hemo 4 | # Defines application settings. 5 | class Settings < Hanami::Settings 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /config/slices/health.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Health 4 | # The health slice configuration. 5 | class Slice < Hanami::Slice 6 | import keys: ["assets"], from: Hanami.app.container, as: :app 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/slices/home.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Home 4 | # The home slice configuration. 5 | class Slice < Hanami::Slice 6 | import keys: ["assets"], from: Hanami.app.container, as: :app 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/slices/tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | # The tasks slice configuration. 5 | class Slice < Hanami::Slice 6 | import keys: ["assets"], from: Hanami.app.container, as: :app 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/hemo/refines/actions/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Hemo 4 | module Refines 5 | module Actions 6 | # Modifies and enhances default Hanami action response behavior. 7 | module Response 8 | refine Hanami::Action::Response do 9 | def with body:, status: 10 | @body = [body] 11 | @status = status 12 | self 13 | end 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/hemo/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dry/types" 4 | 5 | module Hemo 6 | Types = Dry.Types 7 | 8 | # The custom types. 9 | module Types 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hemo", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "hemo", 9 | "version": "0.0.0", 10 | "license": "Hippocratic-2.1", 11 | "dependencies": { 12 | "hanami-assets": "^2.2.0" 13 | } 14 | }, 15 | "node_modules/@esbuild/aix-ppc64": { 16 | "version": "0.19.12", 17 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", 18 | "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", 19 | "cpu": [ 20 | "ppc64" 21 | ], 22 | "optional": true, 23 | "os": [ 24 | "aix" 25 | ], 26 | "engines": { 27 | "node": ">=12" 28 | } 29 | }, 30 | "node_modules/@esbuild/android-arm": { 31 | "version": "0.19.12", 32 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", 33 | "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", 34 | "cpu": [ 35 | "arm" 36 | ], 37 | "optional": true, 38 | "os": [ 39 | "android" 40 | ], 41 | "engines": { 42 | "node": ">=12" 43 | } 44 | }, 45 | "node_modules/@esbuild/android-arm64": { 46 | "version": "0.19.12", 47 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", 48 | "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", 49 | "cpu": [ 50 | "arm64" 51 | ], 52 | "optional": true, 53 | "os": [ 54 | "android" 55 | ], 56 | "engines": { 57 | "node": ">=12" 58 | } 59 | }, 60 | "node_modules/@esbuild/android-x64": { 61 | "version": "0.19.12", 62 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", 63 | "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", 64 | "cpu": [ 65 | "x64" 66 | ], 67 | "optional": true, 68 | "os": [ 69 | "android" 70 | ], 71 | "engines": { 72 | "node": ">=12" 73 | } 74 | }, 75 | "node_modules/@esbuild/darwin-arm64": { 76 | "version": "0.19.12", 77 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", 78 | "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", 79 | "cpu": [ 80 | "arm64" 81 | ], 82 | "optional": true, 83 | "os": [ 84 | "darwin" 85 | ], 86 | "engines": { 87 | "node": ">=12" 88 | } 89 | }, 90 | "node_modules/@esbuild/darwin-x64": { 91 | "version": "0.19.12", 92 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", 93 | "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", 94 | "cpu": [ 95 | "x64" 96 | ], 97 | "optional": true, 98 | "os": [ 99 | "darwin" 100 | ], 101 | "engines": { 102 | "node": ">=12" 103 | } 104 | }, 105 | "node_modules/@esbuild/freebsd-arm64": { 106 | "version": "0.19.12", 107 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", 108 | "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", 109 | "cpu": [ 110 | "arm64" 111 | ], 112 | "optional": true, 113 | "os": [ 114 | "freebsd" 115 | ], 116 | "engines": { 117 | "node": ">=12" 118 | } 119 | }, 120 | "node_modules/@esbuild/freebsd-x64": { 121 | "version": "0.19.12", 122 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", 123 | "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", 124 | "cpu": [ 125 | "x64" 126 | ], 127 | "optional": true, 128 | "os": [ 129 | "freebsd" 130 | ], 131 | "engines": { 132 | "node": ">=12" 133 | } 134 | }, 135 | "node_modules/@esbuild/linux-arm": { 136 | "version": "0.19.12", 137 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", 138 | "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", 139 | "cpu": [ 140 | "arm" 141 | ], 142 | "optional": true, 143 | "os": [ 144 | "linux" 145 | ], 146 | "engines": { 147 | "node": ">=12" 148 | } 149 | }, 150 | "node_modules/@esbuild/linux-arm64": { 151 | "version": "0.19.12", 152 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", 153 | "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", 154 | "cpu": [ 155 | "arm64" 156 | ], 157 | "optional": true, 158 | "os": [ 159 | "linux" 160 | ], 161 | "engines": { 162 | "node": ">=12" 163 | } 164 | }, 165 | "node_modules/@esbuild/linux-ia32": { 166 | "version": "0.19.12", 167 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", 168 | "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", 169 | "cpu": [ 170 | "ia32" 171 | ], 172 | "optional": true, 173 | "os": [ 174 | "linux" 175 | ], 176 | "engines": { 177 | "node": ">=12" 178 | } 179 | }, 180 | "node_modules/@esbuild/linux-loong64": { 181 | "version": "0.19.12", 182 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", 183 | "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", 184 | "cpu": [ 185 | "loong64" 186 | ], 187 | "optional": true, 188 | "os": [ 189 | "linux" 190 | ], 191 | "engines": { 192 | "node": ">=12" 193 | } 194 | }, 195 | "node_modules/@esbuild/linux-mips64el": { 196 | "version": "0.19.12", 197 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", 198 | "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", 199 | "cpu": [ 200 | "mips64el" 201 | ], 202 | "optional": true, 203 | "os": [ 204 | "linux" 205 | ], 206 | "engines": { 207 | "node": ">=12" 208 | } 209 | }, 210 | "node_modules/@esbuild/linux-ppc64": { 211 | "version": "0.19.12", 212 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", 213 | "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", 214 | "cpu": [ 215 | "ppc64" 216 | ], 217 | "optional": true, 218 | "os": [ 219 | "linux" 220 | ], 221 | "engines": { 222 | "node": ">=12" 223 | } 224 | }, 225 | "node_modules/@esbuild/linux-riscv64": { 226 | "version": "0.19.12", 227 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", 228 | "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", 229 | "cpu": [ 230 | "riscv64" 231 | ], 232 | "optional": true, 233 | "os": [ 234 | "linux" 235 | ], 236 | "engines": { 237 | "node": ">=12" 238 | } 239 | }, 240 | "node_modules/@esbuild/linux-s390x": { 241 | "version": "0.19.12", 242 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", 243 | "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", 244 | "cpu": [ 245 | "s390x" 246 | ], 247 | "optional": true, 248 | "os": [ 249 | "linux" 250 | ], 251 | "engines": { 252 | "node": ">=12" 253 | } 254 | }, 255 | "node_modules/@esbuild/linux-x64": { 256 | "version": "0.19.12", 257 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", 258 | "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", 259 | "cpu": [ 260 | "x64" 261 | ], 262 | "optional": true, 263 | "os": [ 264 | "linux" 265 | ], 266 | "engines": { 267 | "node": ">=12" 268 | } 269 | }, 270 | "node_modules/@esbuild/netbsd-x64": { 271 | "version": "0.19.12", 272 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", 273 | "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", 274 | "cpu": [ 275 | "x64" 276 | ], 277 | "optional": true, 278 | "os": [ 279 | "netbsd" 280 | ], 281 | "engines": { 282 | "node": ">=12" 283 | } 284 | }, 285 | "node_modules/@esbuild/openbsd-x64": { 286 | "version": "0.19.12", 287 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", 288 | "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", 289 | "cpu": [ 290 | "x64" 291 | ], 292 | "optional": true, 293 | "os": [ 294 | "openbsd" 295 | ], 296 | "engines": { 297 | "node": ">=12" 298 | } 299 | }, 300 | "node_modules/@esbuild/sunos-x64": { 301 | "version": "0.19.12", 302 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", 303 | "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", 304 | "cpu": [ 305 | "x64" 306 | ], 307 | "optional": true, 308 | "os": [ 309 | "sunos" 310 | ], 311 | "engines": { 312 | "node": ">=12" 313 | } 314 | }, 315 | "node_modules/@esbuild/win32-arm64": { 316 | "version": "0.19.12", 317 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", 318 | "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", 319 | "cpu": [ 320 | "arm64" 321 | ], 322 | "optional": true, 323 | "os": [ 324 | "win32" 325 | ], 326 | "engines": { 327 | "node": ">=12" 328 | } 329 | }, 330 | "node_modules/@esbuild/win32-ia32": { 331 | "version": "0.19.12", 332 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", 333 | "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", 334 | "cpu": [ 335 | "ia32" 336 | ], 337 | "optional": true, 338 | "os": [ 339 | "win32" 340 | ], 341 | "engines": { 342 | "node": ">=12" 343 | } 344 | }, 345 | "node_modules/@esbuild/win32-x64": { 346 | "version": "0.19.12", 347 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", 348 | "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", 349 | "cpu": [ 350 | "x64" 351 | ], 352 | "optional": true, 353 | "os": [ 354 | "win32" 355 | ], 356 | "engines": { 357 | "node": ">=12" 358 | } 359 | }, 360 | "node_modules/@isaacs/cliui": { 361 | "version": "8.0.2", 362 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 363 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 364 | "dependencies": { 365 | "string-width": "^5.1.2", 366 | "string-width-cjs": "npm:string-width@^4.2.0", 367 | "strip-ansi": "^7.0.1", 368 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 369 | "wrap-ansi": "^8.1.0", 370 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 371 | }, 372 | "engines": { 373 | "node": ">=12" 374 | } 375 | }, 376 | "node_modules/@pkgjs/parseargs": { 377 | "version": "0.11.0", 378 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 379 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 380 | "optional": true, 381 | "engines": { 382 | "node": ">=14" 383 | } 384 | }, 385 | "node_modules/ansi-regex": { 386 | "version": "6.0.1", 387 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 388 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 389 | "engines": { 390 | "node": ">=12" 391 | }, 392 | "funding": { 393 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 394 | } 395 | }, 396 | "node_modules/ansi-styles": { 397 | "version": "6.2.1", 398 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 399 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 400 | "engines": { 401 | "node": ">=12" 402 | }, 403 | "funding": { 404 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 405 | } 406 | }, 407 | "node_modules/balanced-match": { 408 | "version": "1.0.2", 409 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 410 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 411 | }, 412 | "node_modules/brace-expansion": { 413 | "version": "2.0.1", 414 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 415 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 416 | "dependencies": { 417 | "balanced-match": "^1.0.0" 418 | } 419 | }, 420 | "node_modules/color-convert": { 421 | "version": "2.0.1", 422 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 423 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 424 | "dependencies": { 425 | "color-name": "~1.1.4" 426 | }, 427 | "engines": { 428 | "node": ">=7.0.0" 429 | } 430 | }, 431 | "node_modules/color-name": { 432 | "version": "1.1.4", 433 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 434 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 435 | }, 436 | "node_modules/cross-spawn": { 437 | "version": "7.0.6", 438 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 439 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 440 | "license": "MIT", 441 | "dependencies": { 442 | "path-key": "^3.1.0", 443 | "shebang-command": "^2.0.0", 444 | "which": "^2.0.1" 445 | }, 446 | "engines": { 447 | "node": ">= 8" 448 | } 449 | }, 450 | "node_modules/eastasianwidth": { 451 | "version": "0.2.0", 452 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 453 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" 454 | }, 455 | "node_modules/emoji-regex": { 456 | "version": "9.2.2", 457 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 458 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" 459 | }, 460 | "node_modules/esbuild": { 461 | "version": "0.19.12", 462 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", 463 | "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", 464 | "hasInstallScript": true, 465 | "bin": { 466 | "esbuild": "bin/esbuild" 467 | }, 468 | "engines": { 469 | "node": ">=12" 470 | }, 471 | "optionalDependencies": { 472 | "@esbuild/aix-ppc64": "0.19.12", 473 | "@esbuild/android-arm": "0.19.12", 474 | "@esbuild/android-arm64": "0.19.12", 475 | "@esbuild/android-x64": "0.19.12", 476 | "@esbuild/darwin-arm64": "0.19.12", 477 | "@esbuild/darwin-x64": "0.19.12", 478 | "@esbuild/freebsd-arm64": "0.19.12", 479 | "@esbuild/freebsd-x64": "0.19.12", 480 | "@esbuild/linux-arm": "0.19.12", 481 | "@esbuild/linux-arm64": "0.19.12", 482 | "@esbuild/linux-ia32": "0.19.12", 483 | "@esbuild/linux-loong64": "0.19.12", 484 | "@esbuild/linux-mips64el": "0.19.12", 485 | "@esbuild/linux-ppc64": "0.19.12", 486 | "@esbuild/linux-riscv64": "0.19.12", 487 | "@esbuild/linux-s390x": "0.19.12", 488 | "@esbuild/linux-x64": "0.19.12", 489 | "@esbuild/netbsd-x64": "0.19.12", 490 | "@esbuild/openbsd-x64": "0.19.12", 491 | "@esbuild/sunos-x64": "0.19.12", 492 | "@esbuild/win32-arm64": "0.19.12", 493 | "@esbuild/win32-ia32": "0.19.12", 494 | "@esbuild/win32-x64": "0.19.12" 495 | } 496 | }, 497 | "node_modules/foreground-child": { 498 | "version": "3.1.1", 499 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", 500 | "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", 501 | "dependencies": { 502 | "cross-spawn": "^7.0.0", 503 | "signal-exit": "^4.0.1" 504 | }, 505 | "engines": { 506 | "node": ">=14" 507 | }, 508 | "funding": { 509 | "url": "https://github.com/sponsors/isaacs" 510 | } 511 | }, 512 | "node_modules/fs-extra": { 513 | "version": "11.2.0", 514 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", 515 | "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", 516 | "dependencies": { 517 | "graceful-fs": "^4.2.0", 518 | "jsonfile": "^6.0.1", 519 | "universalify": "^2.0.0" 520 | }, 521 | "engines": { 522 | "node": ">=14.14" 523 | } 524 | }, 525 | "node_modules/glob": { 526 | "version": "10.3.12", 527 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", 528 | "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", 529 | "dependencies": { 530 | "foreground-child": "^3.1.0", 531 | "jackspeak": "^2.3.6", 532 | "minimatch": "^9.0.1", 533 | "minipass": "^7.0.4", 534 | "path-scurry": "^1.10.2" 535 | }, 536 | "bin": { 537 | "glob": "dist/esm/bin.mjs" 538 | }, 539 | "engines": { 540 | "node": ">=16 || 14 >=14.17" 541 | }, 542 | "funding": { 543 | "url": "https://github.com/sponsors/isaacs" 544 | } 545 | }, 546 | "node_modules/graceful-fs": { 547 | "version": "4.2.11", 548 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 549 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 550 | }, 551 | "node_modules/hanami-assets": { 552 | "version": "2.2.0", 553 | "resolved": "https://registry.npmjs.org/hanami-assets/-/hanami-assets-2.2.0.tgz", 554 | "integrity": "sha512-/vrozjWGty2DHjslEWc4dP+Bd0IxyXIuzXhyrClZN4TRjGxFem+zhWdyZu/TU7GQI6nJRs+EHshuIlaT4yFvTg==", 555 | "license": "MIT", 556 | "dependencies": { 557 | "esbuild": "^0.19.0", 558 | "fs-extra": "^11.1.0", 559 | "glob": "^10.3.3" 560 | }, 561 | "funding": { 562 | "url": "https://github.com/sponsors/hanami" 563 | } 564 | }, 565 | "node_modules/is-fullwidth-code-point": { 566 | "version": "3.0.0", 567 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 568 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 569 | "engines": { 570 | "node": ">=8" 571 | } 572 | }, 573 | "node_modules/isexe": { 574 | "version": "2.0.0", 575 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 576 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 577 | }, 578 | "node_modules/jackspeak": { 579 | "version": "2.3.6", 580 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", 581 | "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", 582 | "dependencies": { 583 | "@isaacs/cliui": "^8.0.2" 584 | }, 585 | "engines": { 586 | "node": ">=14" 587 | }, 588 | "funding": { 589 | "url": "https://github.com/sponsors/isaacs" 590 | }, 591 | "optionalDependencies": { 592 | "@pkgjs/parseargs": "^0.11.0" 593 | } 594 | }, 595 | "node_modules/jsonfile": { 596 | "version": "6.1.0", 597 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", 598 | "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", 599 | "dependencies": { 600 | "universalify": "^2.0.0" 601 | }, 602 | "optionalDependencies": { 603 | "graceful-fs": "^4.1.6" 604 | } 605 | }, 606 | "node_modules/lru-cache": { 607 | "version": "10.2.0", 608 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", 609 | "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", 610 | "engines": { 611 | "node": "14 || >=16.14" 612 | } 613 | }, 614 | "node_modules/minimatch": { 615 | "version": "9.0.4", 616 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", 617 | "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", 618 | "dependencies": { 619 | "brace-expansion": "^2.0.1" 620 | }, 621 | "engines": { 622 | "node": ">=16 || 14 >=14.17" 623 | }, 624 | "funding": { 625 | "url": "https://github.com/sponsors/isaacs" 626 | } 627 | }, 628 | "node_modules/minipass": { 629 | "version": "7.0.4", 630 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", 631 | "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", 632 | "engines": { 633 | "node": ">=16 || 14 >=14.17" 634 | } 635 | }, 636 | "node_modules/path-key": { 637 | "version": "3.1.1", 638 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 639 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 640 | "engines": { 641 | "node": ">=8" 642 | } 643 | }, 644 | "node_modules/path-scurry": { 645 | "version": "1.10.2", 646 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", 647 | "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", 648 | "dependencies": { 649 | "lru-cache": "^10.2.0", 650 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 651 | }, 652 | "engines": { 653 | "node": ">=16 || 14 >=14.17" 654 | }, 655 | "funding": { 656 | "url": "https://github.com/sponsors/isaacs" 657 | } 658 | }, 659 | "node_modules/shebang-command": { 660 | "version": "2.0.0", 661 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 662 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 663 | "dependencies": { 664 | "shebang-regex": "^3.0.0" 665 | }, 666 | "engines": { 667 | "node": ">=8" 668 | } 669 | }, 670 | "node_modules/shebang-regex": { 671 | "version": "3.0.0", 672 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 673 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 674 | "engines": { 675 | "node": ">=8" 676 | } 677 | }, 678 | "node_modules/signal-exit": { 679 | "version": "4.1.0", 680 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 681 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 682 | "engines": { 683 | "node": ">=14" 684 | }, 685 | "funding": { 686 | "url": "https://github.com/sponsors/isaacs" 687 | } 688 | }, 689 | "node_modules/string-width": { 690 | "version": "5.1.2", 691 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 692 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 693 | "dependencies": { 694 | "eastasianwidth": "^0.2.0", 695 | "emoji-regex": "^9.2.2", 696 | "strip-ansi": "^7.0.1" 697 | }, 698 | "engines": { 699 | "node": ">=12" 700 | }, 701 | "funding": { 702 | "url": "https://github.com/sponsors/sindresorhus" 703 | } 704 | }, 705 | "node_modules/string-width-cjs": { 706 | "name": "string-width", 707 | "version": "4.2.3", 708 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 709 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 710 | "dependencies": { 711 | "emoji-regex": "^8.0.0", 712 | "is-fullwidth-code-point": "^3.0.0", 713 | "strip-ansi": "^6.0.1" 714 | }, 715 | "engines": { 716 | "node": ">=8" 717 | } 718 | }, 719 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 720 | "version": "5.0.1", 721 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 722 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 723 | "engines": { 724 | "node": ">=8" 725 | } 726 | }, 727 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 728 | "version": "8.0.0", 729 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 730 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 731 | }, 732 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 733 | "version": "6.0.1", 734 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 735 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 736 | "dependencies": { 737 | "ansi-regex": "^5.0.1" 738 | }, 739 | "engines": { 740 | "node": ">=8" 741 | } 742 | }, 743 | "node_modules/strip-ansi": { 744 | "version": "7.1.0", 745 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 746 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 747 | "dependencies": { 748 | "ansi-regex": "^6.0.1" 749 | }, 750 | "engines": { 751 | "node": ">=12" 752 | }, 753 | "funding": { 754 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 755 | } 756 | }, 757 | "node_modules/strip-ansi-cjs": { 758 | "name": "strip-ansi", 759 | "version": "6.0.1", 760 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 761 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 762 | "dependencies": { 763 | "ansi-regex": "^5.0.1" 764 | }, 765 | "engines": { 766 | "node": ">=8" 767 | } 768 | }, 769 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 770 | "version": "5.0.1", 771 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 772 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 773 | "engines": { 774 | "node": ">=8" 775 | } 776 | }, 777 | "node_modules/universalify": { 778 | "version": "2.0.1", 779 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", 780 | "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", 781 | "engines": { 782 | "node": ">= 10.0.0" 783 | } 784 | }, 785 | "node_modules/which": { 786 | "version": "2.0.2", 787 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 788 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 789 | "dependencies": { 790 | "isexe": "^2.0.0" 791 | }, 792 | "bin": { 793 | "node-which": "bin/node-which" 794 | }, 795 | "engines": { 796 | "node": ">= 8" 797 | } 798 | }, 799 | "node_modules/wrap-ansi": { 800 | "version": "8.1.0", 801 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 802 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 803 | "dependencies": { 804 | "ansi-styles": "^6.1.0", 805 | "string-width": "^5.0.1", 806 | "strip-ansi": "^7.0.1" 807 | }, 808 | "engines": { 809 | "node": ">=12" 810 | }, 811 | "funding": { 812 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 813 | } 814 | }, 815 | "node_modules/wrap-ansi-cjs": { 816 | "name": "wrap-ansi", 817 | "version": "7.0.0", 818 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 819 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 820 | "dependencies": { 821 | "ansi-styles": "^4.0.0", 822 | "string-width": "^4.1.0", 823 | "strip-ansi": "^6.0.0" 824 | }, 825 | "engines": { 826 | "node": ">=10" 827 | }, 828 | "funding": { 829 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 830 | } 831 | }, 832 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 833 | "version": "5.0.1", 834 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 835 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 836 | "engines": { 837 | "node": ">=8" 838 | } 839 | }, 840 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 841 | "version": "4.3.0", 842 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 843 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 844 | "dependencies": { 845 | "color-convert": "^2.0.1" 846 | }, 847 | "engines": { 848 | "node": ">=8" 849 | }, 850 | "funding": { 851 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 852 | } 853 | }, 854 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 855 | "version": "8.0.0", 856 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 857 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" 858 | }, 859 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 860 | "version": "4.2.3", 861 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 862 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 863 | "dependencies": { 864 | "emoji-regex": "^8.0.0", 865 | "is-fullwidth-code-point": "^3.0.0", 866 | "strip-ansi": "^6.0.1" 867 | }, 868 | "engines": { 869 | "node": ">=8" 870 | } 871 | }, 872 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 873 | "version": "6.0.1", 874 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 875 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 876 | "dependencies": { 877 | "ansi-regex": "^5.0.1" 878 | }, 879 | "engines": { 880 | "node": ">=8" 881 | } 882 | } 883 | } 884 | } 885 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hemo", 3 | "description": "", 4 | "version": "0.0.0", 5 | "author": "Brooke Kuhlmann", 6 | "license": "Hippocratic-2.1", 7 | "private": true, 8 | "type": "module", 9 | "keywords": ["ruby", "hanami"], 10 | "dependencies": { 11 | "hanami-assets": "^2.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /public/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Contact: https://alchemists.io/policies/security 2 | Encryption: https://alchemists.io/keybase.txt 3 | Policy: https://alchemists.io/policies/security 4 | Preferred-Languages: en 5 | Canonical: https://alchemists.io/.well-known/security.txt 6 | Permission: none 7 | Expires: 2026-06-15T22:00:00z 8 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 404: Not Found 9 | 10 | 47 | 48 | 49 | 50 |
51 |
52 |

404

53 |

The page you are looking for doesn’t exist.

54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 500: Internal Server Error 9 | 10 | 47 | 48 | 49 | 50 |
51 |
52 |

500

53 |

We’re sorry, but something went wrong.

54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /slices/health/action.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Health 5 | # The slice base action. 6 | class Action < Hemo::Action 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /slices/health/actions/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Health 4 | module Actions 5 | # The show action. 6 | class Show < Health::Action 7 | handle_exception Exception => :down 8 | 9 | def handle(*, response) = response.render view, color: :green 10 | 11 | private 12 | 13 | def down(*, response, _exception) = response.render view, color: :red, status: 503 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /slices/health/templates/layouts/app.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= content_for :title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | <%= favicon_tag app_assets["icon.svg"], 20 | title: "Hemo: Icon", 21 | rel: :icon, 22 | type: "image/svg+xml" %> 23 | 27 | 28 | 29 | 30 | <%= yield %> 31 | 32 | 33 | -------------------------------------------------------------------------------- /slices/health/templates/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Health | Hemo" %> 2 | 3 |
4 |
5 | -------------------------------------------------------------------------------- /slices/health/view.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Health 5 | # The slice base view. 6 | class View < Hemo::View 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /slices/health/views/context.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Health 5 | module Views 6 | # The health slice context. 7 | class Context < Hanami::View::Context 8 | include Deps[app_assets: "app.assets"] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /slices/health/views/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Health 4 | module Views 5 | # The show view. 6 | class Show < Health::View 7 | expose :color 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /slices/home/action.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Home 5 | # The slice base action. 6 | class Action < Hemo::Action 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /slices/home/actions/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Home 4 | module Actions 5 | # The show action. 6 | class Show < Home::Action 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /slices/home/templates/show.html.erb: -------------------------------------------------------------------------------- 1 | <% content_for :title, "Hemo" %> 2 | 3 |
4 | 10 |
11 | 12 |
13 | 18 | 19 |

Hemo

20 |
21 | 22 | 49 | -------------------------------------------------------------------------------- /slices/home/view.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Home 5 | # The slice base view. 6 | class View < Hemo::View 7 | config.layouts_dir = Hanami.app.root.join "app/templates/layouts" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /slices/home/views/context.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Home 5 | module Views 6 | # The home slice context. 7 | class Context < Hanami::View::Context 8 | include Deps[app_assets: "app.assets"] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /slices/home/views/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Home 4 | module Views 5 | # The show view. 6 | class Show < Home::View 7 | expose :ruby_version, default: RUBY_VERSION 8 | expose :hanami_version, default: Hanami::VERSION 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /slices/tasks/action.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Tasks 5 | # The slice base action. 6 | class Action < Hemo::Action 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /slices/tasks/actions/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The create action. 6 | class Create < Tasks::Action 7 | include Deps[ 8 | task_repository: "repositories.task", 9 | user_repository: "repositories.user", 10 | show_view: "views.show", 11 | new_view: "views.new" 12 | ] 13 | 14 | params do 15 | required(:task).hash do 16 | required(:user_id).filled :integer 17 | required(:description).filled :string 18 | end 19 | end 20 | 21 | def handle request, response 22 | parameters = request.params 23 | 24 | if parameters.valid? 25 | task = task_repository.create parameters[:task] 26 | response.render show_view, task: task_repository.find(task.id) 27 | else 28 | response.render new_view, users: user_repository.all, error: parameters.errors[:task] 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /slices/tasks/actions/delete.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The delete action. 6 | class Delete < Tasks::Action 7 | include Deps[repository: "repositories.task"] 8 | 9 | using Hemo::Refines::Actions::Response 10 | 11 | params { required(:id).filled :integer } 12 | 13 | def handle request, response 14 | parameters = request.params 15 | 16 | halt :unprocessable_entity unless parameters.valid? 17 | 18 | repository.delete parameters[:id] 19 | response.with body: "", status: 200 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /slices/tasks/actions/edit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The edit action. 6 | class Edit < Tasks::Action 7 | include Deps[task_repository: "repositories.task", user_repository: "repositories.user"] 8 | 9 | params { required(:id).filled :integer } 10 | 11 | def handle request, response 12 | parameters = request.params 13 | 14 | halt :unprocessable_entity unless parameters.valid? 15 | 16 | response.render view, 17 | task: task_repository.find(parameters[:id]), 18 | users: user_repository.all, 19 | error: Dry::Core::EMPTY_HASH 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /slices/tasks/actions/index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The index action. 6 | class Index < Tasks::Action 7 | include Deps[repository: "repositories.task", search_view: "views.search"] 8 | 9 | params { optional(:query).filled :string } 10 | 11 | def handle request, response 12 | query = request.params[:query] 13 | tasks = repository.find_by_description query 14 | 15 | if request.get_header("HTTP_HX_TRIGGER") == "search" 16 | add_htmx_headers response, query 17 | response.render search_view, tasks: 18 | else 19 | response.render view, tasks: 20 | end 21 | end 22 | 23 | private 24 | 25 | def add_htmx_headers response, query 26 | response.headers["HX-Push-Url"] = routes.path(:tasks_root) if query.empty? 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /slices/tasks/actions/new.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The new action. 6 | class New < Tasks::Action 7 | include Deps[user_repository: "repositories.user"] 8 | 9 | def handle(*, response) 10 | response.render view, users: user_repository.all, error: Dry::Core::EMPTY_HASH 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /slices/tasks/actions/patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The patch action. 6 | class Patch < Tasks::Action 7 | include Deps[repository: "repositories.task", show_view: "views.show"] 8 | 9 | params do 10 | required(:id).filled :integer 11 | optional(:task).hash { optional(:completed).filled :string } 12 | end 13 | 14 | def initialize(now: Time.now.utc, **) 15 | @now = now 16 | super(**) 17 | end 18 | 19 | def handle request, response 20 | parameters = request.params 21 | 22 | halt :unprocessable_entity unless parameters.valid? 23 | 24 | id = parameters[:id] 25 | 26 | case parameters.to_h 27 | in task: {completed: _} then response.render show_view, task: complete(id) 28 | else response.render show_view, task: uncomplete(id) 29 | end 30 | end 31 | 32 | private 33 | 34 | attr_reader :now 35 | 36 | def complete id 37 | repository.update id, completed_at: now 38 | repository.find id 39 | end 40 | 41 | def uncomplete id 42 | repository.update id, completed_at: nil 43 | repository.find id 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /slices/tasks/actions/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The show action. 6 | class Show < Tasks::Action 7 | include Deps[repository: "repositories.task"] 8 | 9 | params { required(:id).filled :integer } 10 | 11 | def handle request, response 12 | parameters = request.params 13 | halt :unprocessable_entity unless parameters.valid? 14 | response.render view, task: repository.find(parameters[:id]) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /slices/tasks/actions/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Actions 5 | # The update action. 6 | class Update < Tasks::Action 7 | include Deps[ 8 | task_repository: "repositories.task", 9 | user_repository: "repositories.user", 10 | show_view: "views.show", 11 | edit_view: "views.edit" 12 | ] 13 | 14 | params do 15 | required(:id).filled :integer 16 | required(:task).hash do 17 | optional(:user_id).filled :integer 18 | optional(:description).filled :string 19 | optional(:completed).maybe :string 20 | end 21 | end 22 | 23 | def initialize(now: Time.now.utc, **) 24 | @now = now 25 | super(**) 26 | end 27 | 28 | def handle request, response 29 | parameters = request.params 30 | task = task_repository.find parameters[:id] 31 | 32 | case parameters 33 | in {id: Integer, **nil} | {} then halt :unprocessable_entity 34 | in id: _, task: {description: content} unless content.empty? 35 | save task, parameters, response 36 | else edit task, parameters, response 37 | end 38 | end 39 | 40 | private 41 | 42 | attr_reader :now 43 | 44 | def save task, parameters, response 45 | id = task.id 46 | task_repository.update id, 47 | user_id: user_repository.find(parameters.dig(:task, :user_id)).id, 48 | description: parameters.dig(:task, :description), 49 | completed_at: (now if parameters.dig :task, :completed) 50 | response.render show_view, task: task_repository.find(id) 51 | end 52 | 53 | def edit task, parameters, response 54 | response.render edit_view, 55 | task:, 56 | users: user_repository.all, 57 | error: parameters.errors[:task] 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /slices/tasks/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { transform: rotate(0deg); } 3 | 100% { transform: rotate(360deg); } 4 | } 5 | 6 | .tasks-page { 7 | --font-family: Verdana; 8 | --color-black: hsl(0, 0%, 0%); 9 | --color-blue: hsl(220, 59%, 48%); 10 | --color-green: hsl(122, 45%, 45%); 11 | --color-grey: hsl(0, 0%, 85%); 12 | --color-powder: hsl(201, 70%, 96%); 13 | --color-red: hsl(5, 69%, 46%); 14 | --color-white: hsl(0, 0%, 100%); 15 | --shadow-color: 0deg 0% 50%; 16 | 17 | font-family: var(--font-family); 18 | margin: 1rem 5rem; 19 | 20 | [hx-swap-oob="true"] { 21 | display: none; 22 | } 23 | 24 | .button { 25 | border-radius: 0.5rem; 26 | border: 0; 27 | cursor: pointer; 28 | display: flex; 29 | font-size: 1rem; 30 | margin: 0; 31 | padding: 0.2rem 0.5rem; 32 | text-decoration: none; 33 | } 34 | 35 | .button-act { 36 | background-color: var(--color-blue); 37 | color: var(--color-white); 38 | } 39 | 40 | .button-accept { 41 | background-color: var(--color-green); 42 | color: var(--color-white); 43 | } 44 | 45 | .button-decline { 46 | background-color: var(--color-red); 47 | color: var(--color-white); 48 | } 49 | 50 | .header { 51 | align-items: center; 52 | display: flex; 53 | flex-wrap: wrap; 54 | gap: 1rem; 55 | justify-content: space-between; 56 | margin: 2rem 0; 57 | 58 | .label { 59 | margin: 0; 60 | } 61 | 62 | .search { 63 | display: flex; 64 | gap: 0.5rem; 65 | 66 | .loader { 67 | animation: spin 1.5s linear infinite; 68 | border-radius: 50%; 69 | border: 0.3rem solid var(--color-grey); 70 | border-top: 0.3rem solid var(--color-green); 71 | height: 1rem; 72 | width: 1rem; 73 | } 74 | } 75 | } 76 | 77 | .body, .tasks { 78 | display: flex; 79 | flex-direction: column; 80 | gap: 1rem; 81 | } 82 | 83 | .body { 84 | .empty { 85 | align-items: center; 86 | display: flex; 87 | font-size: 1.2rem; 88 | justify-content: center; 89 | } 90 | 91 | .new { 92 | align-self: flex-start; 93 | } 94 | } 95 | 96 | .tasks { 97 | gap: 1rem; 98 | list-style-type: none; 99 | margin: 0 0 1rem 0; 100 | padding: 0; 101 | } 102 | 103 | .task { 104 | background-color: var(--color-powder); 105 | border-radius: 0.5rem; 106 | box-shadow: 107 | 0.6px 0.6px 1px hsl(var(--shadow-color) / 0.15), 108 | 1.4px 1.4px 2.2px -0.6px hsl(var(--shadow-color) / 0.15), 109 | 2.7px 2.6px 4.2px -1.2px hsl(var(--shadow-color) / 0.15), 110 | 5.2px 5.1px 8.2px -1.9px hsl(var(--shadow-color) / 0.15), 111 | 9.9px 9.8px 15.7px -2.5px hsl(var(--shadow-color) / 0.15); 112 | gap: 0.5rem; 113 | padding: 0.5rem; 114 | } 115 | 116 | .task-new.htmx-swapping { 117 | opacity: 100; 118 | transition: opacity 1s ease-in; 119 | } 120 | 121 | .task-new.htmx-added { 122 | opacity: 0; 123 | } 124 | 125 | .task-new { 126 | opacity: 1; 127 | transition: opacity 1s ease-out; 128 | } 129 | 130 | .task-completed { 131 | background-color: var(--color-grey); 132 | } 133 | 134 | .task-completed .item { 135 | text-decoration: line-through; 136 | } 137 | 138 | .task { 139 | .body { 140 | align-items: center; 141 | display: grid; 142 | grid-template: 1fr / 1fr 1fr 1fr; 143 | 144 | .group { 145 | display: flex; 146 | flex-direction: column; 147 | 148 | .error { 149 | color: var(--color-white); 150 | margin: 0; 151 | } 152 | } 153 | 154 | .item { 155 | display: flex; 156 | gap: 0.5rem; 157 | } 158 | 159 | .check { 160 | background: none; 161 | border: none; 162 | } 163 | 164 | .label { 165 | font-weight: 800; 166 | 167 | &::after { 168 | content: ":"; 169 | padding: 0 0.5rem 0 0; 170 | } 171 | } 172 | 173 | .field { 174 | align-items: center; 175 | display: flex; 176 | font-size: 1rem; 177 | gap: 0.5rem; 178 | } 179 | 180 | .input { 181 | border: 0.1rem solid var(--color-grey); 182 | border-radius: 0.5rem; 183 | font-size: 1rem; 184 | padding: 0.3rem; 185 | width: 100%; 186 | } 187 | 188 | .select { 189 | font-size: 1rem; 190 | background-color: var(--color-white); 191 | border: 0.1rem solid var(--color-grey); 192 | border-radius: 0.5rem; 193 | padding: 0.3rem 0.5rem; 194 | } 195 | 196 | .actions { 197 | align-items: center; 198 | display: flex; 199 | gap: 0.5rem; 200 | justify-content: flex-end; 201 | } 202 | 203 | .error { 204 | .input { 205 | border: 0.2rem solid var(--color-red); 206 | } 207 | 208 | .message { 209 | color: var(--color-red); 210 | margin: 0; 211 | padding: 0.2rem 0 0.1rem 0.3rem; 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | -------------------------------------------------------------------------------- /slices/tasks/assets/js/app.js: -------------------------------------------------------------------------------- 1 | import "../css/app.css"; 2 | -------------------------------------------------------------------------------- /slices/tasks/config/db/migrate/20230820202820_create_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ROM::SQL.migration do 4 | up do 5 | create_table :users do 6 | primary_key :id, :bigint 7 | column :name, :text, null: false 8 | column :email, :text, null: false 9 | column :created_at, :timestamp, null: false, default: Sequel::CURRENT_TIMESTAMP 10 | column :updated_at, :timestamp, null: false, default: Sequel::CURRENT_TIMESTAMP 11 | end 12 | end 13 | 14 | down { drop_table :users } 15 | end 16 | -------------------------------------------------------------------------------- /slices/tasks/config/db/migrate/20230820202821_create_tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ROM::SQL.migration do 4 | up do 5 | create_table :tasks do 6 | primary_key :id, :bigint 7 | foreign_key :user_id, type: :bigint, null: false 8 | column :description, :text, null: false 9 | column :completed_at, :timestamp, null: true 10 | column :created_at, :timestamp, null: false, default: Sequel::CURRENT_TIMESTAMP 11 | column :updated_at, :timestamp, null: false, default: Sequel::CURRENT_TIMESTAMP 12 | end 13 | end 14 | 15 | down { drop_table :tasks } 16 | end 17 | -------------------------------------------------------------------------------- /slices/tasks/config/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | rom = Tasks::Slice["db.rom"] 4 | users = rom.relations.users 5 | 6 | users.insert name: "Jane Smith", email: "jsmith@example.com" 7 | users.insert name: "Jack Doe", email: "jdoe@example.com" 8 | -------------------------------------------------------------------------------- /slices/tasks/config/db/structure.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 17.0 6 | -- Dumped by pg_dump version 17.0 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET transaction_timeout = 0; 12 | SET client_encoding = 'UTF8'; 13 | SET standard_conforming_strings = on; 14 | SELECT pg_catalog.set_config('search_path', '', false); 15 | SET check_function_bodies = false; 16 | SET xmloption = content; 17 | SET client_min_messages = warning; 18 | SET row_security = off; 19 | 20 | SET default_tablespace = ''; 21 | 22 | SET default_table_access_method = heap; 23 | 24 | -- 25 | -- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - 26 | -- 27 | 28 | CREATE TABLE public.schema_migrations ( 29 | filename text NOT NULL 30 | ); 31 | 32 | 33 | -- 34 | -- Name: tasks; Type: TABLE; Schema: public; Owner: - 35 | -- 36 | 37 | CREATE TABLE public.tasks ( 38 | id bigint NOT NULL, 39 | user_id bigint NOT NULL, 40 | description text NOT NULL, 41 | completed_at timestamp without time zone, 42 | created_at timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text) NOT NULL, 43 | updated_at timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text) NOT NULL 44 | ); 45 | 46 | 47 | -- 48 | -- Name: tasks_id_seq; Type: SEQUENCE; Schema: public; Owner: - 49 | -- 50 | 51 | ALTER TABLE public.tasks ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( 52 | SEQUENCE NAME public.tasks_id_seq 53 | START WITH 1 54 | INCREMENT BY 1 55 | NO MINVALUE 56 | NO MAXVALUE 57 | CACHE 1 58 | ); 59 | 60 | 61 | -- 62 | -- Name: users; Type: TABLE; Schema: public; Owner: - 63 | -- 64 | 65 | CREATE TABLE public.users ( 66 | id bigint NOT NULL, 67 | name text NOT NULL, 68 | email text NOT NULL, 69 | created_at timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text) NOT NULL, 70 | updated_at timestamp without time zone DEFAULT (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'::text) NOT NULL 71 | ); 72 | 73 | 74 | -- 75 | -- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - 76 | -- 77 | 78 | ALTER TABLE public.users ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY ( 79 | SEQUENCE NAME public.users_id_seq 80 | START WITH 1 81 | INCREMENT BY 1 82 | NO MINVALUE 83 | NO MAXVALUE 84 | CACHE 1 85 | ); 86 | 87 | 88 | -- 89 | -- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - 90 | -- 91 | 92 | ALTER TABLE ONLY public.schema_migrations 93 | ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (filename); 94 | 95 | 96 | -- 97 | -- Name: tasks tasks_pkey; Type: CONSTRAINT; Schema: public; Owner: - 98 | -- 99 | 100 | ALTER TABLE ONLY public.tasks 101 | ADD CONSTRAINT tasks_pkey PRIMARY KEY (id); 102 | 103 | 104 | -- 105 | -- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: - 106 | -- 107 | 108 | ALTER TABLE ONLY public.users 109 | ADD CONSTRAINT users_pkey PRIMARY KEY (id); 110 | 111 | 112 | -- 113 | -- PostgreSQL database dump complete 114 | -- 115 | 116 | SET search_path TO "$user", public; 117 | 118 | INSERT INTO schema_migrations (filename) VALUES 119 | ('20230820202820_create_users.rb'), 120 | ('20230820202821_create_tasks.rb'); 121 | -------------------------------------------------------------------------------- /slices/tasks/db/relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module DB 5 | # The slice database base relation. 6 | class Relation < Hemo::DB::Relation 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /slices/tasks/db/repository.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module DB 5 | # The slice database base repository. 6 | class Repository < Hemo::DB::Repository 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /slices/tasks/db/struct.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module DB 5 | # The slice database base struct. 6 | class Struct < Hemo::DB::Struct 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /slices/tasks/relations/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Relations 5 | # The task relation. 6 | class Task < DB::Relation 7 | schema :tasks, infer: true do 8 | associations { belongs_to :user } 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /slices/tasks/relations/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Relations 5 | # The user relation. 6 | class User < DB::Relation 7 | schema :users, infer: true do 8 | associations { has_many :tasks } 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /slices/tasks/repositories/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Repositories 5 | # The task repository. 6 | class Task < DB::Repository[:tasks] 7 | commands :create, update: :by_pk, delete: :by_pk 8 | 9 | def find(id) = tasks.combine(:user).by_pk(id).one 10 | 11 | def find_by_description text 12 | tasks.combine(:user) 13 | .where { description.ilike "%#{text}%" } 14 | .to_a 15 | end 16 | 17 | def all 18 | tasks.combine(:user) 19 | .order { created_at.asc } 20 | .to_a 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /slices/tasks/repositories/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Repositories 5 | # The user repository. 6 | class User < DB::Repository 7 | commands :create, update: :by_pk, delete: :by_pk 8 | 9 | def find(id) = users.by_pk(id).one 10 | 11 | def all 12 | users.order { created_at.desc } 13 | .to_a 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /slices/tasks/templates/_description.html.erb: -------------------------------------------------------------------------------- 1 | <% if error.key? :description %> 2 |
3 | 4 |

<%= message %>

5 |
6 | <% else %> 7 | 8 | <% end %> 9 | -------------------------------------------------------------------------------- /slices/tasks/templates/_rows.html.erb: -------------------------------------------------------------------------------- 1 | <% tasks.each do |task| %> 2 | <%= task.render :task %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /slices/tasks/templates/_search.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /slices/tasks/templates/_task.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 | 8 | autocomplete="off" 9 | hx-patch="/tasks/<%= task.id %>"/> 10 | 11 | 12 | <%= task.description %> 13 | 14 |
    15 | 16 |
    17 | <%= task.assignee %> 18 |
    19 | 20 |
    21 | <%= edit_link task.id %> 22 | 23 |
    24 | <%= delete_button task.id %> 25 |
    26 |
    27 |
    28 |
  • 29 | -------------------------------------------------------------------------------- /slices/tasks/templates/_users.html.erb: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /slices/tasks/templates/edit.html.erb: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 | 8 | autocomplete="off"/> 9 | 10 | <%= scope(:description, content: task.description, error:).render %> 11 |
    12 | 13 | <%= render :users %> 14 | 15 | 16 | <%= edit_save_button %> 17 | <%= edit_cancel_link task.id %> 18 | 19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /slices/tasks/templates/index.html.erb: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | Home 4 | | 5 | Tasks 6 |

    7 | 8 | <%= render :search %> 9 |
    10 | 11 |
    12 | <% if tasks.empty? %> 13 |

    ☀️ Congratulations, all tasks are complete!

    14 | <% end %> 15 | 16 |
      17 | <%= render :rows, tasks: %> 18 |
    19 | 20 | <%= new_link %> 21 |
    22 | -------------------------------------------------------------------------------- /slices/tasks/templates/layouts/app.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Tasks | Hemo 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | <%= favicon_tag app_assets["icon.svg"], title: "Hemo: Icon", rel: :icon, type: "image/svg+xml" %> 22 | 26 | <%= tag.link title: "Hemo: Manifest", rel: :manifest, href: app_assets["manifest.webmanifest"] %> 27 | 28 | <%= stylesheet_tag "app", title: "Hemo: Stylesheet" %> 29 | 30 | 34 | 35 | 39 | 40 | 41 | 42 | <%= yield %> 43 | 44 | 45 | -------------------------------------------------------------------------------- /slices/tasks/templates/new.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
  • 4 |
    5 |
    6 | 7 | <%= scope(:description, content: "", error:).render %> 8 |
    9 | 10 |
    11 | 12 | <%= render :users %> 13 |
    14 | 15 |
    16 | <%= create_save_button %> 17 | <%= create_cancel_link %> 18 |
    19 |
    20 |
  • 21 | -------------------------------------------------------------------------------- /slices/tasks/templates/search.html.erb: -------------------------------------------------------------------------------- 1 | <%= render "rows", tasks: %> 2 | -------------------------------------------------------------------------------- /slices/tasks/templates/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= task.render :task %> 2 | -------------------------------------------------------------------------------- /slices/tasks/view.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Tasks 5 | # The slice base view. 6 | class View < Hemo::View 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /slices/tasks/views/context.rb: -------------------------------------------------------------------------------- 1 | # auto_register: false 2 | # frozen_string_literal: true 3 | 4 | module Tasks 5 | module Views 6 | # The task slice context. 7 | class Context < Hanami::View::Context 8 | include Deps[app_assets: "app.assets"] 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /slices/tasks/views/edit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Views 5 | # The edit view. 6 | class Edit < Tasks::View 7 | expose :task 8 | expose :users 9 | expose :error 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /slices/tasks/views/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "htmx" 4 | 5 | module Tasks 6 | module Views 7 | # The view helpers. 8 | module Helpers 9 | def search_input 10 | tag.input( 11 | id: "search", 12 | type: "search", 13 | name: "query", 14 | value: "", 15 | **HTMX[ 16 | get: routes.path(:tasks_root), 17 | trigger: "search, keyup delay:200ms changed", 18 | target: "next .tasks", 19 | push_url: true, 20 | indicator: ".loader" 21 | ] 22 | ) 23 | end 24 | 25 | def new_link 26 | path = routes.path :tasks_new 27 | 28 | link_to( 29 | "New", 30 | path, 31 | class: "new button button-accept", 32 | **HTMX[ 33 | get: path, 34 | trigger: "click, keyup[ctrlKey&&key=='n'] from:body", 35 | target: ".tasks", 36 | swap: "beforeend settle:0.1s" 37 | ] 38 | ) 39 | end 40 | 41 | def edit_link id 42 | path = routes.path(:tasks_edit, id:) 43 | 44 | link_to( 45 | "Edit", 46 | path, 47 | class: "button button-act", 48 | **HTMX[get: path, swap: "outerHTML swap:0s"] 49 | ) 50 | end 51 | 52 | def edit_save_button 53 | tag.input type: :submit, value: "Save", class: "button button-accept" 54 | end 55 | 56 | def edit_cancel_link id 57 | path = routes.path(:tasks_show, id:) 58 | 59 | link_to( 60 | "Cancel", 61 | path, 62 | class: "button button-decline", 63 | **HTMX[trigger: "click, keyup[key=='Escape'] from:closest .task", get: path] 64 | ) 65 | end 66 | 67 | def delete_button id 68 | tag.button( 69 | "Delete", 70 | type: "submit", 71 | class: "button button-decline", 72 | **HTMX[target: "closest .task", delete: routes.path(:tasks_delete, id:)] 73 | ) 74 | end 75 | 76 | def create_save_button 77 | tag.input( 78 | name: :commit, 79 | type: :submit, 80 | value: "Save", 81 | class: "button button-accept", 82 | **HTMX[ 83 | trigger: :click, 84 | target: "closest .task", 85 | swap: "outerHTML swap:0s", 86 | post: routes.path(:tasks_root) 87 | ] 88 | ) 89 | end 90 | 91 | def create_cancel_link 92 | link_to "Cancel", 93 | routes.path(:tasks_root), 94 | class: "button button-decline", 95 | data: {remove: true} 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /slices/tasks/views/index.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Views 5 | # The index view. 6 | class Index < Tasks::View 7 | expose :tasks 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /slices/tasks/views/new.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Views 5 | # The new view. 6 | class New < Tasks::View 7 | expose :users 8 | expose :error 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /slices/tasks/views/parts/task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Views 5 | module Parts 6 | # The task presentation logic. 7 | class Task < Hanami::View::Part 8 | def assignee = user.name 9 | 10 | def checked = ("checked" if completed_at) 11 | 12 | def css_class = completed_at ? "task task-completed" : "task" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /slices/tasks/views/scopes/description.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "refinements/array" 4 | 5 | module Tasks 6 | module Views 7 | module Scopes 8 | # The description specific behavior for partials. 9 | class Description < Hanami::View::Scope 10 | using Refinements::Array 11 | 12 | def value = content 13 | 14 | def message = (error[:description].to_sentence if error.key? :description) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /slices/tasks/views/search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Views 5 | # The search view. 6 | class Search < Tasks::View 7 | expose :tasks 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /slices/tasks/views/show.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tasks 4 | module Views 5 | # The show view. 6 | class Show < Tasks::View 7 | expose :task 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/features/home_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Home", :web do 4 | it "renders home page" do 5 | visit "/" 6 | expect(page).to have_content("Hemo") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/features/tasks_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe "Tasks", :db, :js, :web do 6 | let(:user) { Factory[:user, name: "Jane Doe", email: "jdoe@example.com"] } 7 | let(:task) { Factory[:task, user:, description: "A test task"] } 8 | let(:tasks_path) { Hanami.app["routes"].path :tasks_root } 9 | 10 | it "displays no tasks when there are none" do 11 | visit tasks_path 12 | expect(page).to have_content("Congratulations") 13 | end 14 | 15 | it "displays task" do 16 | task 17 | visit tasks_path 18 | expect(page).to have_content("A test task") 19 | end 20 | 21 | it "successfully creates a new task" do 22 | user 23 | visit tasks_path 24 | click_link "New" 25 | 26 | select "Jane Doe", from: "task[user_id]" 27 | fill_in "task[description]", with: "This is a test" 28 | click_button "Save" 29 | 30 | expect(page).to have_content("This is a test") 31 | end 32 | 33 | it "fails to create task with errors" do 34 | user 35 | visit tasks_path 36 | click_link "New" 37 | 38 | select "Jane Doe", from: "task[user_id]" 39 | click_button "Save" 40 | 41 | expect(page).to have_content("must be filled") 42 | end 43 | 44 | it "returns to index when canceling new task" do 45 | visit tasks_path 46 | click_link "New" 47 | click_link "Cancel" 48 | sleep 0.5 # For htmx swap delay. 49 | 50 | expect(page).to have_no_content("Cancel") 51 | end 52 | 53 | it "successfully completes a task" do 54 | task 55 | visit tasks_path 56 | check "complete" 57 | 58 | expect(find(".task-completed").text).to include(task.description) 59 | end 60 | 61 | it "successfully edits a task" do 62 | task 63 | visit tasks_path 64 | click_link "Edit" 65 | fill_in "task[description]", with: "An edited task" 66 | click_button "Save" 67 | 68 | expect(page).to have_content("An edited task") 69 | end 70 | 71 | it "is unable to edit a task with errors" do 72 | task 73 | visit tasks_path 74 | click_link "Edit" 75 | fill_in "task[description]", with: nil 76 | click_button "Save" 77 | 78 | expect(page).to have_content("must be filled") 79 | end 80 | 81 | it "successfully destroys a task" do 82 | task 83 | visit tasks_path 84 | click_button "Delete" 85 | sleep 0.3 # For htmx swap delay. 86 | 87 | expect(page).to have_no_content(task.description) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/hanami_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "capybara/cuprite" 4 | require "capybara/rspec" 5 | require "database_cleaner/sequel" 6 | require "dry/monads" 7 | require "rack/test" 8 | require "rom-factory" 9 | require "spec_helper" 10 | 11 | ENV["HANAMI_ENV"] = "test" 12 | 13 | require "hanami/prepare" 14 | 15 | using Refinements::Pathname 16 | 17 | Tasks.const_set :Structs, Module.new 18 | Factory = ROM::Factory.configure { |config| config.rom = Tasks::Slice["db.rom"] } 19 | 20 | ENV["LD_PRELOAD"] = nil 21 | Capybara.app = Hanami.app 22 | Capybara.server = :puma, {Silent: true, Threads: "0:1"} 23 | Capybara.javascript_driver = :cuprite 24 | Capybara.save_path = Hanami.app.root.join "tmp/capybara" 25 | Capybara.register_driver :cuprite do |app| 26 | browser_options = {"disable-gpu" => nil, "disable-dev-shm-usage" => nil, "no-sandbox" => nil} 27 | Capybara::Cuprite::Driver.new app, browser_options:, window_size: [1920, 1080] 28 | end 29 | 30 | Pathname.require_tree SPEC_ROOT.join("support/factories") 31 | 32 | RSpec.configure do |config| 33 | config.include Capybara::DSL, Capybara::RSpecMatchers, :web 34 | config.include Rack::Test::Methods, type: :request 35 | 36 | config.include_context "with Hanami application", type: :request 37 | 38 | databases = proc do 39 | Hanami.app.slices.with_nested.prepend(Hanami.app).each.with_object Set.new do |slice, dbs| 40 | next unless slice.key? "db.rom" 41 | 42 | dbs.merge slice["db.rom"].gateways.values.map(&:connection).to_enum 43 | end 44 | end 45 | 46 | config.before :suite do 47 | databases.call.each do |db| 48 | DatabaseCleaner[:sequel, db:].clean_with :truncation, except: ["schema_migrations"] 49 | end 50 | end 51 | 52 | config.prepend_before :each, :db do |example| 53 | databases.call.each do |db| 54 | DatabaseCleaner[:sequel, db:].strategy = example.metadata[:js] ? :truncation : :transaction 55 | DatabaseCleaner[:sequel, db:].start 56 | end 57 | end 58 | 59 | config.append_after :each, :db do 60 | databases.call.each { |db| DatabaseCleaner[:sequel, db:].clean } 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/lib/hemo/refines/actions/response_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Hemo::Refines::Actions::Response do 6 | using described_class 7 | 8 | subject(:response) { Hanami::Action::Response.new request:, config: {} } 9 | 10 | let :request do 11 | Rack::MockRequest.env_for("/").then { |env| Hanami::Action::Request.new env:, params: {} } 12 | end 13 | 14 | describe "#with" do 15 | it "answers response with given body and status" do 16 | expect(response.with(body: "Danger!", status: 400)).to have_attributes( 17 | body: ["Danger!"], 18 | status: 400 19 | ) 20 | end 21 | 22 | it "answers itself" do 23 | expect(response.with(body: "Danger!", status: 400)).to be_a(Hanami::Action::Response) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/slices/health/actions/show_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Health::Actions::Show do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers green background" do 10 | expect(action.call({}).body.first).to include( 11 | %(
    \n
    ) 12 | ) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/create_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::Create, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | let(:user) { Factory[:user] } 10 | 11 | it "answers 200 OK status with valid parameters" do 12 | response = action.call task: {user_id: user.id, description: "Test"} 13 | expect(response.status).to eq(200) 14 | end 15 | 16 | it "answers errors with invalid parameters" do 17 | response = action.call task: {} 18 | expect(response.body.to_s).to include("is missing") 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/delete_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::Delete, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers 200 OK status and blank body with valid parameters" do 10 | response = action.call id: Factory[:task].id 11 | expect(response).to have_attributes(status: 200, body: [""]) 12 | end 13 | 14 | it "answers 422 Unprocessable Entity with invalid parameters" do 15 | response = action.call Hash.new 16 | expect(response.status).to eq(422) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/edit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::Edit, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers 200 OK status with valid parameters" do 10 | response = action.call id: Factory[:task].id 11 | expect(response.status).to eq(200) 12 | end 13 | 14 | it "answers errors with invalid parameters" do 15 | response = action.call Hash.new 16 | expect(response.status).to eq(422) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/index_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | require "htmx" 5 | 6 | RSpec.describe Tasks::Actions::Index, :db do 7 | subject(:action) { described_class.new } 8 | 9 | describe "#call" do 10 | before { Factory[:task] } 11 | 12 | it "answers 200 OK status" do 13 | response = action.call Hash.new 14 | expect(response.status).to eq(200) 15 | end 16 | 17 | it "answers HTMX Push URL response header when search query is blank" do 18 | response = Rack::MockRequest.new(action).get "/tasks", 19 | "HTTP_HX_TRIGGER" => "search", 20 | params: {query: ""} 21 | 22 | expect(response.headers).to include("HX-Push-Url" => "/tasks") 23 | end 24 | 25 | it "answers no HTMX response headers when search query is filled" do 26 | response = Rack::MockRequest.new(action).get "/tasks", 27 | "HTTP_HX_TRIGGER" => "search", 28 | params: {query: "test"} 29 | 30 | headers = HTMX::Headers::Response.for(**response.headers) 31 | expect(headers).to eq(HTMX::Headers::Response.new) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/new_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::New, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers 200 OK status" do 10 | response = action.call Hash.new 11 | expect(response.status).to eq(200) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/patch_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::Patch, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | let(:task) { Factory[:task] } 10 | 11 | it "answers 200 OK status with ID only" do 12 | response = action.call id: task.id 13 | expect(response.status).to eq(200) 14 | end 15 | 16 | it "answers 200 OK status with ID and complete date/time" do 17 | response = action.call id: task.id, task: {completed: "2023-01-27T22:50:37+00:00"} 18 | expect(response.status).to eq(200) 19 | end 20 | 21 | it "answers errors with no ID" do 22 | response = action.call id: nil 23 | expect(response.status).to eq(422) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/show_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::Show, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | it "answers 200 OK status with valid parameters" do 10 | response = action.call id: Factory[:task].id 11 | expect(response.status).to eq(200) 12 | end 13 | 14 | it "answers 422 Unprocessable Entity with invalid parameters" do 15 | response = action.call Hash.new 16 | expect(response.status).to eq(422) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/slices/tasks/actions/update_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Actions::Update, :db do 6 | subject(:action) { described_class.new } 7 | 8 | describe "#call" do 9 | let(:task) { Factory[:task] } 10 | 11 | it "answers 200 OK status with user ID, description, and completed" do 12 | user = Factory[:user] 13 | response = action.call id: task.id, 14 | task: {user_id: user.id, description: "Test", completed: "on"} 15 | expect(response.status).to eq(200) 16 | end 17 | 18 | it "answers 200 OK status with user ID and description only" do 19 | user = Factory[:user] 20 | response = action.call id: task.id, task: {user_id: user.id, description: "Test"} 21 | expect(response.status).to eq(200) 22 | end 23 | 24 | it "answers errors with missing description" do 25 | response = action.call id: task.id, task: {description: ""} 26 | expect(response.body.to_s).to include("must be filled") 27 | end 28 | 29 | it "answers error when given ID only" do 30 | response = action.call id: task.id 31 | expect(response.status).to eq(422) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/slices/tasks/repositories/task_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Repositories::Task, :db do 6 | subject(:repository) { described_class.new } 7 | 8 | let(:task) { Factory[:task] } 9 | 10 | describe "#find" do 11 | it "answers record by ID" do 12 | expect(repository.find(task.id)).to eq(task) 13 | end 14 | 15 | it "answers nil for unknown ID" do 16 | expect(repository.find(13)).to be(nil) 17 | end 18 | end 19 | 20 | describe "#find_by_description" do 21 | it "answers record by description" do 22 | task = Factory[:task, description: "This is a test"] 23 | expect(repository.find_by_description("this")).to contain_exactly(task) 24 | end 25 | 26 | it "answers empty array unknown description" do 27 | expect(repository.find_by_description(13)).to eq([]) 28 | end 29 | end 30 | 31 | describe "#all" do 32 | it "answers all records" do 33 | task 34 | expect(repository.all).to contain_exactly(task) 35 | end 36 | 37 | it "answers empty array when records don't exist" do 38 | expect(repository.all).to eq([]) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/slices/tasks/views/parts/task_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Views::Parts::Task do 6 | subject(:part) { described_class.new value: task } 7 | 8 | let(:task) { Factory.structs[:task, id: 1, user:] } 9 | let(:user) { Factory.structs[:user, name: "Jane Doe"] } 10 | 11 | describe "#assignee" do 12 | it "answers user" do 13 | expect(part.assignee).to eq("Jane Doe") 14 | end 15 | end 16 | 17 | describe "#checked" do 18 | it "answers checked when completed" do 19 | allow(task).to receive(:completed_at).and_return(Time.now.utc) 20 | expect(part.checked).to eq("checked") 21 | end 22 | 23 | it "answers nil when not completed" do 24 | expect(part.checked).to be(nil) 25 | end 26 | end 27 | 28 | describe "#css_class" do 29 | it "answers completed when completed" do 30 | allow(task).to receive(:completed_at).and_return(Time.now.utc) 31 | expect(part.css_class).to eq("task task-completed") 32 | end 33 | 34 | it "answers default class when not completed" do 35 | expect(part.css_class).to eq("task") 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/slices/tasks/views/scopes/description_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Views::Scopes::Description do 6 | subject(:scope) { described_class.new locals:, rendering: view.new.rendering } 7 | 8 | let(:locals) { {content: "Test", error: {description: ["Danger!"]}} } 9 | 10 | let :view do 11 | Class.new Hanami::View do 12 | config.paths = SPEC_ROOT 13 | config.template = "placeholder" 14 | end 15 | end 16 | 17 | describe "#value" do 18 | it "answers value" do 19 | expect(scope.value).to eq("Test") 20 | end 21 | end 22 | 23 | describe "#message" do 24 | it "answers error message when error description exists" do 25 | expect(scope.message).to eq("Danger!") 26 | end 27 | 28 | it "answers nil when error description doesn't exist" do 29 | locals[:error] = {} 30 | expect(scope.message).to be(nil) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/slices/tasks/views/show_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "hanami_helper" 4 | 5 | RSpec.describe Tasks::Views::Show, :db do 6 | subject(:view) { described_class.new } 7 | 8 | describe "#call" do 9 | let(:task) { Factory.structs[:task, id: 1, user:] } 10 | let(:user) { Factory.structs[:user, name: "Jane Doe"] } 11 | 12 | it "renders view" do 13 | expect(view.call(task:).to_s).to include(%(