diff --git a/.ameba.yml b/.ameba.yml
new file mode 100644
index 0000000..d7b2b9a
--- /dev/null
+++ b/.ameba.yml
@@ -0,0 +1,24 @@
+# This configuration file was generated by `ameba --gen-config`
+# on 2023-01-30 12:35:15 UTC using Ameba version 1.4.0.
+# The point is for the user to remove these configuration records
+# one by one as the reported problems are removed from the code base.
+
+# Problems found: 2
+# Run `ameba --only Lint/UselessAssign` for details
+Lint/UselessAssign:
+ Description: Disallows useless variable assignments
+ Excluded:
+ - spec/view_spec.cr
+ Enabled: true
+ Severity: Warning
+
+# Problems found: 6
+# Run `ameba --only Lint/NotNil` for details
+Lint/NotNil:
+ Description: Identifies usage of `not_nil!` calls
+ Excluded:
+ - src/kemal/param_parser.cr
+ - src/kemal/static_file_handler.cr
+ - src/kemal/config.cr
+ Enabled: true
+ Severity: Warning
diff --git a/.github/workflows/ameba.yml b/.github/workflows/ameba.yml
deleted file mode 100644
index e321d6a..0000000
--- a/.github/workflows/ameba.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: Ameba
-
-on:
- push:
- pull_request:
-
-permissions:
- contents: read
-
-jobs:
- lint:
- runs-on: ubuntu-latest
-
- steps:
- - name: Download source
- uses: actions/checkout@v6
-
- - name: Run Ameba Linter
- uses: crystal-ameba/github-action@master
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e65750..f473bda 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,3 +30,47 @@ jobs:
- name: Run specs
run: |
crystal spec
+
+ format:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ crystal: [latest, nightly]
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal }}
+
+ - name: Download source
+ uses: actions/checkout@v4
+
+ - name: Check formatting
+ run: crystal tool format --check
+
+ ameba:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ crystal: [latest]
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: Install Crystal
+ uses: crystal-lang/install-crystal@v1
+ with:
+ crystal: ${{ matrix.crystal }}
+
+ - name: Download source
+ uses: actions/checkout@v4
+
+ - name: Install dependencies
+ run: shards install
+
+ - name: Run ameba linter
+ run: bin/ameba
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2382024..130192c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,156 +1,3 @@
-# 1.10.1 (24-03-2026)
-
-- Add `shutdown_timeout` configuration for graceful shutdown: after `Kemal.stop`, Kemal can wait before exit so in-flight work can finish [#745](https://github.com/kemalcr/kemal/pull/745). Thanks @sdogruyol :pray:
-
-```crystal
-Kemal.config.shutdown_timeout = 10.seconds
-```
-
-# 1.10.0 (03-03-2026)
-
-- Add modular `Kemal::Router` with namespaced routing, scoped filters, WebSocket support and flexible mounting while keeping the existing DSL fully compatible [#731](https://github.com/kemalcr/kemal/pull/731). Thanks @sdogruyol :pray:
-
-```crystal
-require "kemal"
-
-api = Kemal::Router.new
-
-api.namespace "/users" do
- get "/" do |env|
- env.json({users: ["alice", "bob"]})
- end
-
- get "/:id" do |env|
- env.text "user #{env.params.url["id"]}"
- end
-end
-
-mount "/api/v1", api
-
-Kemal.run
-```
-
-- Add `use` keyword for registering global and path-specific middleware, including support for arrays and insertion at a specific position in the handler chain [#734](https://github.com/kemalcr/kemal/pull/734). Thanks @sdogruyol :pray:
-
-```crystal
-require "kemal"
-
-# Path-specific middlewares for /api routes
-use "/api", [CORSHandler.new, AuthHandler.new]
-
-get "/" do
- "Public home"
-end
-
-get "/api/users" do |env|
- env.json({users: ["alice", "bob"]})
-end
-
-Kemal.run
-```
-
-- Enhance response helpers to provide chainable JSON/HTML/text/XML helpers, `HTTP::Status` support and the ability to halt execution from a chained response for concise API error handling [#733](https://github.com/kemalcr/kemal/pull/733), [#735](https://github.com/kemalcr/kemal/pull/735), [#736](https://github.com/kemalcr/kemal/pull/736). Thanks @sdogruyol and @mamantoha :pray:
-
-```crystal
-require "kemal"
-
-get "/users" do |env|
- # Default JSON response
- env.json({users: ["alice", "bob"]})
-end
-
-post "/users" do |env|
- # Symbol-based HTTP::Status and chained JSON
- env.status(:created).json({id: 1, created: true})
-end
-
-get "/admin" do |env|
- # Halt immediately with HTML response
- halt env.status(403).html("
Forbidden
")
-end
-
-get "/api/users" do |env|
- # Custom content type (JSON:API)
- env.json({data: ["alice", "bob"]}, content_type: "application/vnd.api+json")
-end
-
-Kemal.run
-```
-
-- Ensure global wildcard filters always execute while keeping namespace filters isolated to their routes [#737](https://github.com/kemalcr/kemal/pull/737). Thanks @mamantoha :pray:
-- Fix CLI SSL validation and expand CLI option parsing specs [#738](https://github.com/kemalcr/kemal/pull/738). Thanks @sdogruyol :pray:
-- Make route LRU cache concurrency-safe with Mutex [#739](https://github.com/kemalcr/kemal/pull/739). Thanks @sdogruyol :pray:
-- Add `raw_body` to ParamParser for multi-handler body access (e.g. kemal-session) [#740](https://github.com/kemalcr/kemal/pull/740). Thanks @sdogruyol :pray:
-
-```crystal
-post "/" do |env|
- raw = env.params.raw_body # raw body, multiple handlers can call it
- env.params.body["name"] # parsed body
-end
-```
-
-- Fix OverrideMethodHandler route cache bug when using `_method` override [#741](https://github.com/kemalcr/kemal/pull/741), [#742](https://github.com/kemalcr/kemal/pull/742). Thanks @skojin and @sdogruyol :pray:
-
-# 1.9.0 (28-01-2026)
-
-- Crystal 1.19.0 support :tada:
-- ***(SECURITY)*** Limit maximum request body size to avoid DoS attacks [#730](https://github.com/kemalcr/kemal/pull/730). Thanks @sdogruyol :pray:
-- Optimize JSON parameter parsing by directly using the request body IO. Thanks @sdogruyol :pray:
-
-# 1.8.0 (07-11-2025)
-
-- Enhance HEAD request handling by caching GET route lookups and optimize path construction using string interpolation for improved performance [#728](https://github.com/kemalcr/kemal/pull/728). Thanks @sdogruyol :pray:
-- Improve error messages [#726](https://github.com/kemalcr/kemal/pull/726). Thanks @sdogruyol :pray:
-- Optimize route and websocket lookups by caching results to reduce redundant processing in the HTTP server context [#725](https://github.com/kemalcr/kemal/pull/725). Thanks @sdogruyol :pray:
-- Replace full-flush Route cache with LRU and add a configurable max cache size [#724](https://github.com/kemalcr/kemal/pull/724). Thanks @sdogruyol :pray:
-
-# 1.7.3 (02-10-2025)
-
-- Refactor [#719](https://github.com/kemalcr/kemal/pull/719). Thanks @sdogruyol :pray:
-- Improve Kemal test suite. Thanks @sdogruyol :pray:
-
-# 1.7.2 (04-08-2025)
-
-- Move Kemal::Handler logic into separate module [#717](https://github.com/kemalcr/kemal/pull/717). Thanks @syeopite :pray:
-- Refactor server binding logic to avoid binding in test environment [#719](https://github.com/kemalcr/kemal/pull/719). Thanks @sdogruyol :pray:
-
-# 1.7.1 (14-04-2025)
-
-- Improve `StaticFileHandler` to align with latest Crystal implementation [#711](https://github.com/kemalcr/kemal/pull/711). Thanks @sdogruyol :pray:
-
-# 1.7.0 (14-04-2025)
-
-- ***(SECURITY)*** Fix a Path Traversal Security issue in `StaticFileHandler`. [See](https://packetstorm.news/files/id/190294/) for more details. Thanks a lot @ahmetumitbayram :pray:
-- Crystal 1.16.0 support :tada:
-- Add ability to add handlers for raised exceptions [#688](https://github.com/kemalcr/kemal/pull/688). Thanks @syeopite :pray:
-
-```crystal
-require "kemal"
-
-class NewException < Exception
-end
-
-get "/" do | env |
- raise NewException.new()
-end
-
-error NewException do | env |
- "An error occured!"
-end
-
-Kemal.run
-```
-
-- Add `all_files` method to `params` to support multiple file uploads in names ending with `[]` [#701](https://github.com/kemalcr/kemal/pull/701). Thanks @sdogruyol :pray:
-
-```crystal
-images = env.params.all_files["images[]"]?
-```
-
-- Embrace Crystal standard Log for logging [#705](https://github.com/kemalcr/kemal/pull/705). Thanks @hugopl :pray:
-- Cleanup temporary files for file uploads [#707](https://github.com/kemalcr/kemal/pull/707). Thanks @sdogruyol :pray:
-- Implement multiple partial ranges [#708](https://github.com/kemalcr/kemal/pull/708). Thanks @sdogruyol :pray:
-
# 1.6.0 (12-10-2024)
- Crystal 1.14.0 support :tada:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 356782c..0000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Contributing to Kemal
-
-Thank you for your interest in contributing to Kemal! We love pull requests from everyone.
-
-## Getting Started
-
-1. **Fork** the repository on GitHub.
-2. **Clone** your fork locally:
- ```bash
- git clone https://github.com/YOUR_USERNAME/kemal.git
- cd kemal
- ```
-3. **Install dependencies**:
- ```bash
- shards install
- ```
-
-## Running Tests
-
-Before submitting a pull request, please ensure that all tests pass.
-
-```bash
-crystal spec
-```
-
-## Code Style
-
-Kemal follows the standard Crystal code style. Please ensure your code is formatted correctly before committing.
-
-```bash
-crystal tool format
-```
-
-## Submitting a Pull Request
-
-1. Create a new branch for your feature or bug fix:
- ```bash
- git checkout -b my-new-feature
- ```
-2. Commit your changes with descriptive commit messages.
-3. Push your branch to your fork:
- ```bash
- git push origin my-new-feature
- ```
-4. Open a **Pull Request** on the main Kemal repository.
-5. Describe your changes and link to any relevant issues.
-
-## Reporting Bugs
-
-If you find a bug, please open an issue on GitHub with:
-- A clear title and description.
-- Steps to reproduce the issue.
-- The version of Kemal and Crystal you are using.
-
-## Feature Requests
-
-We welcome new ideas! Please open an issue to discuss your feature request before implementing it.
-
-Thank you for contributing to Kemal! π
diff --git a/README.md b/README.md
index de923a3..aba67b4 100644
--- a/README.md
+++ b/README.md
@@ -2,32 +2,45 @@
# Kemal
-Kemal is the Fast, Effective, Simple Web Framework for Crystal. It's perfect for building Web Applications and APIs with minimal code.
+Lightning Fast, Super Simple web framework.
**THIS IS A FORK OF KEMAL. DIRECT TO FORK.MD FOR SPECIFICS ON THIS FORK.**
[](https://github.com/kemalcr/kemal/actions/workflows/ci.yml)
+[](https://gitter.im/sdogruyol/kemal?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-## Why Kemal?
+# Super Simple β‘οΈ
-- π **Lightning Fast**: Built on Crystal, known for C-like performance
-- π‘ **Super Simple**: Minimal code needed to get started
-- π **Feature Rich**: Everything you need for modern web development
-- π§ **Flexible**: Easy to extend with middleware support
+```ruby
+require "kemal"
-## Quick Start
+# Matches GET "http://host:port/"
+get "/" do
+ "Hello World!"
+end
-1. First, make sure you have [Crystal installed](https://crystal-lang.org/install/).
+# Creates a WebSocket handler.
+# Matches "ws://host:port/socket"
+ws "/socket" do |socket|
+ socket.send "Hello from Kemal!"
+end
-2. Create a new Crystal application and step into it:
-
-```bash
-crystal init app my-kemal-app
-cd my-kemal-app
+Kemal.run
```
-3. Add Kemal to your app's `shard.yml`:
->>>>>>> upstream/master
+Start your application!
+
+```
+crystal src/kemal_sample.cr
+```
+
+Go to _http://localhost:3000_
+
+Check [documentation](http://kemalcr.com) or [samples](https://github.com/kemalcr/kemal/tree/master/samples) for more.
+
+# Installation
+
+Add this to your application's `shard.yml`:
```yaml
dependencies:
@@ -35,78 +48,22 @@ dependencies:
github: kemalcr/kemal
```
-4. Replace the contents of `src/my_kemal_app.cr` with your first Kemal app:
+See also [Getting Started](http://kemalcr.com/guide/).
-```crystal
-require "kemal"
+# Features
-# Basic route - responds to GET "http://localhost:3000/"
-get "/" do
- "Hello World!"
-end
+- Support all REST verbs
+- Websocket support
+- Request/Response context, easy parameter handling
+- Middleware support
+- Built-in JSON support
+- Built-in static file serving
+- Built-in view templating via [ECR](https://crystal-lang.org/api/ECR.html)
-# JSON API example
-get "/api/status" do |env|
- env.response.content_type = "application/json"
- {"status": "ok"}.to_json
-end
+# Documentation
-# WebSocket support
-ws "/chat" do |socket|
- socket.send "Hello from Kemal WebSocket!"
-end
+You can read the documentation at the official site [kemalcr.com](http://kemalcr.com)
-Kemal.run
-```
+## Thanks
-5. Install dependencies and run your application:
-
-```bash
-shards install
-crystal run src/my_kemal_app.cr
-```
-
-6. Visit [http://localhost:3000](http://localhost:3000) - That's it! π
-
-## Key Features
-
-- π **High-performance by default**: Built on Crystal with a thin abstraction layer so you can serve a large number of requests with low latency and low memory footprint.
-- π **Full REST & HTTP support**: Handle all HTTP verbs (GET, POST, PUT, PATCH, DELETE, etc.) with a straightforward routing DSL.
-- π **WebSocket & real-time**: First-class WebSocket support for building chats, dashboards and other real-time experiences.
-- π¦ **JSON-first APIs**: Native JSON handling makes building JSON APIs and microservices feel natural.
-- ποΈ **Static assets made easy**: Serve static files (assets, uploads, SPA bundles) efficiently from the same application.
-- π **Template engine included**: Built-in ECR template engine for serverβrendered HTML when you need it.
-- π **Composable middleware**: Flexible middleware system to add logging, auth, rate limiting, metrics and more.
-- π― **Ergonomic request/response API**: Simple access to params, headers, cookies and bodies via a clear context object.
-- πͺ **Session management**: Easy session handling with [kemal-session](https://github.com/kemalcr/kemal-session), suitable for production apps.
-
-## Philosophy
-
-Kemal aims to be a simple, fast and reliable foundation for building production-grade web applications and APIs in Crystal.
-
-- **Simple core, powerful building blocks**: The core is intentionally simple and easy to reason about. Most power comes from Crystal itself and from middleware, not from hidden magic.
-- **Performance as a baseline, not a feature**: Crystal's native speed means high performance is the default. Kemal keeps abstractions thin so you stay close to the metal when you need to.
-- **Minimal assumptions, maximum flexibility**: Kemal does not force a specific ORM, template engine, or project layout. You are free to choose the tools that fit your application and your team.
-- **Batteries within reason**: Kemal ships with the essentials (routing, middleware, templates, static files, request/response helpers) while keeping advanced concerns in separate shards you can opt into as your app grows.
-
-Kemal is designed to feel familiar if you come from popular web frameworks, while embracing Crystal's strengths and keeping your application code straightforward, maintainable, and ready for production.
-
-## Learning Resources
-
-- π [Official Documentation](http://kemalcr.com)
-- π» [Example Applications](https://github.com/kemalcr/kemal/tree/master/examples)
-- π [Kemal Guide](http://kemalcr.com/guide/)
-- π¬ [Community Chat](https://discord.gg/prSVAZJEpz)
-
-
-## Contributing
-
-We love contributions! Please read our [Contributing Guide](CONTRIBUTING.md) to get started.
-
-## Acknowledgments
-
-Special thanks to Manas for their work on [Frank](https://github.com/manastech/frank).
-
-## License
-
-Kemal is released under the MIT License.
+Thanks to Manas for their awesome work on [Frank](https://github.com/manastech/frank).
diff --git a/examples/cookies/app.cr b/examples/cookies/app.cr
deleted file mode 100644
index 644e01a..0000000
--- a/examples/cookies/app.cr
+++ /dev/null
@@ -1,67 +0,0 @@
-require "kemal"
-
-# This example demonstrates different ways to work with cookies in Kemal
-
-# Route to set various types of cookies
-get "/set-cookies" do |env|
- # Basic cookie with just name and value
- basic_cookie = HTTP::Cookie.new(
- name: "BasicCookie",
- value: "Hello from Kemal!"
- )
-
- # Secure cookie with additional security options
- secure_cookie = HTTP::Cookie.new(
- name: "SecureCookie",
- value: "Sensitive Data",
- http_only: true, # Cookie cannot be accessed via JavaScript
- secure: true, # Cookie only sent over HTTPS
- path: "/", # Cookie available for all paths
- expires: Time.local + Time::Span.new(days: 7) # Cookie expires in 7 days
- )
-
- # Session cookie that expires when browser closes
- session_cookie = HTTP::Cookie.new(
- name: "SessionCookie",
- value: "Temporary",
- http_only: true
- )
-
- # Add all cookies to response
- env.response.cookies << basic_cookie
- env.response.cookies << secure_cookie
- env.response.cookies << session_cookie
-
- "Cookies have been set! Visit /show-cookies to view them."
-end
-
-# Route to display current cookies
-get "/show-cookies" do |env|
- cookies = env.request.cookies
- response = String.build do |str|
- str << "Current Cookies:
"
- str << ""
- cookies.each do |cookie|
- str << "- #{cookie.name}: #{cookie.value}
"
- end
- str << "
"
- end
- response
-end
-
-# Route to delete a specific cookie
-get "/delete-cookie/:name" do |env|
- cookie_name = env.params.url["name"]
-
- # Set cookie with immediate expiration to delete it
- delete_cookie = HTTP::Cookie.new(
- name: cookie_name,
- value: "",
- expires: Time.local - 1.day
- )
-
- env.response.cookies << delete_cookie
- "Cookie '#{cookie_name}' has been deleted!"
-end
-
-Kemal.run
diff --git a/examples/cors/app.cr b/examples/cors/app.cr
deleted file mode 100644
index ae5b067..0000000
--- a/examples/cors/app.cr
+++ /dev/null
@@ -1,17 +0,0 @@
-require "kemal"
-
-# Configure headers for static files using Kemal's static_headers helper
-static_headers do |response, filepath, filestat|
- # For HTML files, add CORS header to allow requests from example.com
- # This restricts access to HTML files to only that domain
- if filepath =~ /\.html$/
- response.headers.add("Access-Control-Allow-Origin", "example.com")
- end
-
- # Add Content-Size header for all static files
- # This helps clients know the file size before downloading
- response.headers.add("Content-Size", filestat.size.to_s)
-end
-
-# Start the Kemal web server
-Kemal.run
diff --git a/examples/file-download/app.cr b/examples/file-download/app.cr
deleted file mode 100644
index dd23487..0000000
--- a/examples/file-download/app.cr
+++ /dev/null
@@ -1,18 +0,0 @@
-require "kemal"
-
-# Define a route for the root path "/" that will handle file downloads
-get "/" do |env|
- # Use Kemal's send_file helper to stream a file to the client
- # Parameters:
- # - env: The HTTP environment containing request/response data
- # - "/path/to/your_file": The path to the file you want to download
- #
- # send_file will:
- # - Set appropriate Content-Type header based on file extension
- # - Stream the file in chunks to handle large files efficiently
- # - Set Content-Disposition header for browser download behavior
- send_file env, "/path/to/your_file"
-end
-
-# Start the Kemal web server
-Kemal.run
diff --git a/examples/file-upload/app.cr b/examples/file-upload/app.cr
deleted file mode 100644
index 7259ab0..0000000
--- a/examples/file-upload/app.cr
+++ /dev/null
@@ -1,25 +0,0 @@
-require "kemal"
-
-# Handle file uploads via POST request to /upload endpoint
-post "/upload" do |env|
- # Get the uploaded file from the "image" field in the form
- # The file is initially stored in a temporary location
- uploaded_file = env.params.files["image"].tempfile
-
- # Construct the destination path where we'll save the file
- # - Kemal.config.public_folder is the configured public directory
- # - "uploads/" is the subdirectory where we'll store uploads
- # - File.basename gets just the filename from the temp file path
- uploaded_file_path = ::File.join [Kemal.config.public_folder, "uploads/", File.basename(uploaded_file.path)]
-
- # Open the destination file for writing and copy the uploaded file to it
- File.open(uploaded_file_path, "w") do |file|
- IO.copy(uploaded_file, file)
- end
-
- # Return a simple success message
- "Upload ok"
-end
-
-# Start the Kemal server
-Kemal.run
diff --git a/examples/hello-world/app.cr b/examples/hello-world/app.cr
deleted file mode 100644
index fa687db..0000000
--- a/examples/hello-world/app.cr
+++ /dev/null
@@ -1,7 +0,0 @@
-require "kemal"
-
-get "/" do
- "Hello Kemal!"
-end
-
-Kemal.run
diff --git a/examples/http-basic-auth/app.cr b/examples/http-basic-auth/app.cr
deleted file mode 100644
index 5cd2ca3..0000000
--- a/examples/http-basic-auth/app.cr
+++ /dev/null
@@ -1,18 +0,0 @@
-require "kemal"
-require "kemal-basic-auth"
-
-# Enable HTTP Basic Authentication
-# This will protect all routes with username/password authentication
-# - username: "username"
-# - password: "password"
-basic_auth "username", "password"
-
-# Define a route for the root path "/"
-get "/" do |_|
- # This route will only execute if authentication is successful
- # Otherwise, the browser will show a login prompt
- "This is shown if basic auth successful."
-end
-
-# Start the Kemal web server
-Kemal.run
diff --git a/examples/http-basic-auth/custom-handler.cr b/examples/http-basic-auth/custom-handler.cr
deleted file mode 100644
index 0911973..0000000
--- a/examples/http-basic-auth/custom-handler.cr
+++ /dev/null
@@ -1,23 +0,0 @@
-require "kemal-basic-auth"
-
-# Create a custom authentication handler by inheriting from Kemal::BasicAuth::Handler
-class CustomAuthHandler < Kemal::BasicAuth::Handler
- # Specify which routes should be protected by basic auth
- # In this case, only /dashboard and /admin routes will require authentication
- only ["/dashboard", "/admin"]
-
- # Override the call method to implement custom authentication logic
- def call(context)
- # Skip authentication if the current route is not in the protected routes list
- # This allows other routes to be accessed without authentication
- return call_next(context) unless only_match?(context)
-
- # Call the parent class's authentication logic for protected routes
- # This will prompt for username/password and validate credentials
- super
- end
-end
-
-# Register our custom authentication handler with Kemal
-# This enables basic auth for the specified routes
-Kemal.config.auth_handler = CustomAuthHandler
diff --git a/examples/json-api/app.cr b/examples/json-api/app.cr
deleted file mode 100644
index af9ae63..0000000
--- a/examples/json-api/app.cr
+++ /dev/null
@@ -1,72 +0,0 @@
-require "kemal"
-require "json"
-
-# Set JSON content type for all routes
-before_all do |env|
- env.response.content_type = "application/json"
-end
-
-# In-memory storage for users
-USERS = [] of Hash(String, JSON::Any)
-
-# GET - List all users
-get "/users" do |_|
- USERS.to_json
-end
-
-# GET - Get a specific user by index
-get "/users/:id" do |env|
- id = env.params.url["id"].to_i
-
- if id < USERS.size
- USERS[id].to_json
- else
- env.response.status_code = 404
- {error: "User not found"}.to_json
- end
-end
-
-# POST - Create a new user
-post "/users" do |env|
- # Parse request body as JSON
- # ameba:disable Lint/NotNil
- user = JSON.parse(env.request.body.not_nil!.gets_to_end)
- # ameba:enable Lint/NotNil
- USERS << user.as_h
-
- env.response.status_code = 201
- user.to_json
-end
-
-# PUT - Update a user
-put "/users/:id" do |env|
- id = env.params.url["id"].to_i
-
- if id < USERS.size
- # Parse request body as JSON
- # ameba:disable Lint/NotNil
- updated_user = JSON.parse(env.request.body.not_nil!.gets_to_end)
- # ameba:enable Lint/NotNil
- USERS[id] = updated_user.as_h
- updated_user.to_json
- else
- env.response.status_code = 404
- {error: "User not found"}.to_json
- end
-end
-
-# DELETE - Remove a user
-delete "/users/:id" do |env|
- id = env.params.url["id"].to_i
-
- if id < USERS.size
- deleted_user = USERS.delete_at(id)
- deleted_user.to_json
- else
- env.response.status_code = 404
- {error: "User not found"}.to_json
- end
-end
-
-# Start the Kemal web server
-Kemal.run
diff --git a/examples/json-mapping/app.cr b/examples/json-mapping/app.cr
deleted file mode 100644
index 08b4ba7..0000000
--- a/examples/json-mapping/app.cr
+++ /dev/null
@@ -1,32 +0,0 @@
-require "kemal"
-require "json"
-
-# Define a User class that can be created from JSON data
-class User
- # Include JSON::Serializable to add JSON parsing capabilities
- # This allows converting JSON strings to User objects and vice versa
- include JSON::Serializable
-
- # Define properties that will be mapped from JSON
- # These properties must match the keys in the incoming JSON
- property username : String # User's username as a string
- property password : String # User's password as a string
-end
-
-# Handle POST requests to the root path "/"
-post "/" do |env|
- # Parse the request body as JSON and create a User object
- # env.request.body contains the raw JSON data
- # not_nil! ensures the body exists
- # User.from_json converts the JSON string to a User object
- # ameba:disable Lint/NotNil
- user = User.from_json env.request.body.not_nil!
- # ameba:enable Lint/NotNil
-
- # Convert the user object back to JSON and return it
- # This creates a JSON object with username and password fields
- {username: user.username, password: user.password}.to_json
-end
-
-# Start the Kemal web server
-Kemal.run
diff --git a/examples/mysql-db/app.cr b/examples/mysql-db/app.cr
deleted file mode 100644
index 14045cc..0000000
--- a/examples/mysql-db/app.cr
+++ /dev/null
@@ -1,56 +0,0 @@
-require "kemal"
-require "db"
-require "mysql"
-
-# Initialize a single DB connection
-DB_URL = "mysql://root:password@localhost:3306/mydb"
-DBC = DB.open(DB_URL)
-
-# Example User model
-class User
- include JSON::Serializable # To render json in HTTP::Response
- include DB::Serializable # To serialize from DB::ResultSet
-
- property id : Int32
- property name : String
- property email : String
-
- def initialize(@id, @name, @email)
- end
-end
-
-# List all users
-get "/users" do |_|
- # Serialize ResultSet
- users = User.from_rs(DBC.query("SELECT * FROM users"))
-
- # Return users array as JSON response
- users.to_json
-end
-
-# Create a new user
-post "/users" do |env|
- name = env.params.json["name"].as(String)
- email = env.params.json["email"].as(String)
-
- user = User.from_rs(DBC.query("INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email", name, email)).first
-
- {message: "User created with id: #{user.id}"}.to_json
-end
-
-# Delete a user
-delete "/users/:id" do |env|
- id = env.params.url["id"].to_i
-
- # Delete user and check if any rows were affected
- result = DBC.exec "DELETE FROM users WHERE id = ?", id
-
- if result.rows_affected > 0
- {message: "User deleted successfully"}.to_json
- else
- env.response.status_code = 404
- {message: "User not found"}.to_json
- end
-end
-
-Kemal.run
diff --git a/examples/postgresql-db/app.cr b/examples/postgresql-db/app.cr
deleted file mode 100644
index 01aa629..0000000
--- a/examples/postgresql-db/app.cr
+++ /dev/null
@@ -1,56 +0,0 @@
-require "kemal"
-require "db"
-require "pg"
-
-# Initialize a single DB connection
-DB_URL = "postgres://postgres:postgres@localhost:5432/mydb"
-DBC = DB.open(DB_URL)
-
-# Example User model
-class User
- include JSON::Serializable # To render json in HTTP::Response
- include DB::Serializable # To serialize from DB::ResultSet
-
- property id : Int32
- property name : String
- property email : String
-
- def initialize(@id, @name, @email)
- end
-end
-
-# List all users
-get "/users" do |_|
- # Serialize ResultSet
- users = User.from_rs(DBC.query("SELECT * FROM users"))
-
- # Return users array as JSON response
- users.to_json
-end
-
-# Create a new user
-post "/users" do |env|
- name = env.params.json["name"].as(String)
- email = env.params.json["email"].as(String)
-
- user = User.from_rs(DBC.query("INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email", name, email)).first
-
- {message: "User created with id: #{user.id}"}.to_json
-end
-
-# Delete a user
-delete "/users/:id" do |env|
- id = env.params.url["id"].to_i
-
- # Delete user and check if any rows were affected
- result = DBC.exec "DELETE FROM users WHERE id = $1", id
-
- if result.rows_affected > 0
- {message: "User deleted successfully"}.to_json
- else
- env.response.status_code = 404
- {message: "User not found"}.to_json
- end
-end
-
-Kemal.run
diff --git a/examples/redis/app.cr b/examples/redis/app.cr
deleted file mode 100644
index c7a8492..0000000
--- a/examples/redis/app.cr
+++ /dev/null
@@ -1,58 +0,0 @@
-require "kemal"
-require "redis"
-
-# Initialize Redis client
-REDIS = Redis.new(host: "localhost", port: 6379)
-
-# Store a value
-post "/store/:key" do |env|
- key = env.params.url["key"]
- value = env.params.json["value"].as(String)
-
- REDIS.set(key, value)
- {message: "Value stored successfully"}.to_json
-end
-
-# Retrieve a value
-get "/get/:key" do |env|
- key = env.params.url["key"]
-
- if value = REDIS.get(key)
- {key: key, value: value}.to_json
- else
- env.response.status_code = 404
- {message: "Key not found"}.to_json
- end
-end
-
-# Delete a value
-delete "/:key" do |env|
- key = env.params.url["key"]
-
- if REDIS.del(key) > 0
- {message: "Key deleted successfully"}.to_json
- else
- env.response.status_code = 404
- {message: "Key not found"}.to_json
- end
-end
-
-# Increment a counter
-post "/incr/:key" do |env|
- key = env.params.url["key"]
- new_value = REDIS.incr(key)
-
- {key: key, value: new_value}.to_json
-end
-
-# Store with expiration
-post "/store_temp/:key" do |env|
- key = env.params.url["key"]
- value = env.params.json["value"].as(String)
- ttl = env.params.json["ttl"].as(Int64)
-
- REDIS.setex(key, ttl, value)
- {message: "Value stored with expiration"}.to_json
-end
-
-Kemal.run
diff --git a/examples/reuse-port/app.cr b/examples/reuse-port/app.cr
deleted file mode 100644
index b059252..0000000
--- a/examples/reuse-port/app.cr
+++ /dev/null
@@ -1,19 +0,0 @@
-require "kemal"
-
-# Define a simple route that returns a message
-get "/" do
- "Reusing port 3000"
-end
-
-# Start Kemal with custom server configuration
-Kemal.run do |config|
- # Get the server instance from the config
- # ameba:disable Lint/NotNil
- server = config.server.not_nil!
- # ameba:enable Lint/NotNil
-
- # Bind the server to port 3000 with reuse_port enabled
- # reuse_port: true allows multiple processes to listen on the same port
- # This is useful for load balancing across multiple worker processes
- server.bind_tcp "0.0.0.0", 3000, reuse_port: true
-end
diff --git a/examples/unix-domain-socket/app.cr b/examples/unix-domain-socket/app.cr
deleted file mode 100644
index aae5973..0000000
--- a/examples/unix-domain-socket/app.cr
+++ /dev/null
@@ -1,14 +0,0 @@
-require "kemal"
-
-# Start Kemal with custom server configuration to use Unix Domain Socket
-Kemal.run do |config|
- # Get the server instance from the config
- # ameba:disable Lint/NotNil
- server = config.server.not_nil!
- # ameba:enable Lint/NotNil
-
- # Bind the server to a Unix Domain Socket instead of TCP port
- # Unix Domain Sockets provide faster inter-process communication on the same machine
- # They are commonly used when the client and server are on the same host
- server.bind_unix "path/to/socket.sock"
-end
diff --git a/examples/websocket-chat/app.cr b/examples/websocket-chat/app.cr
deleted file mode 100644
index b7e380d..0000000
--- a/examples/websocket-chat/app.cr
+++ /dev/null
@@ -1,33 +0,0 @@
-require "kemal"
-
-# Array to store chat message history
-messages = [] of String
-# Array to keep track of connected WebSocket clients
-sockets = [] of HTTP::WebSocket
-
-# Create WebSocket endpoint at root path "/"
-ws "/" do |socket|
- # Add newly connected client socket to our sockets array
- sockets.push socket
-
- # Handle incoming messages from clients
- socket.on_message do |message|
- # Store the new message in history
- messages.push message
- # Broadcast the updated message history to all connected clients
- sockets.each do |a_socket|
- a_socket.send messages.to_json
- end
- end
-
- # Handle client disconnection
- socket.on_close do |_|
- # Remove disconnected client's socket from our array
- sockets.delete(socket)
- # Log disconnection event
- puts "Closing Socket: #{socket}"
- end
-end
-
-# Start the Kemal server
-Kemal.run
diff --git a/samples/hello_world.cr b/samples/hello_world.cr
new file mode 100644
index 0000000..c04f1d5
--- /dev/null
+++ b/samples/hello_world.cr
@@ -0,0 +1,8 @@
+require "kemal"
+
+# Set root. If not specified the default content_type is 'text'
+get "/" do
+ "Hello Kemal!"
+end
+
+Kemal.run
diff --git a/samples/json_api.cr b/samples/json_api.cr
new file mode 100644
index 0000000..0132c14
--- /dev/null
+++ b/samples/json_api.cr
@@ -0,0 +1,11 @@
+require "kemal"
+require "json"
+
+# You can easily access the context and set content_type like 'application/json'.
+# Look how easy to build a JSON serving API.
+get "/" do |env|
+ env.response.content_type = "application/json"
+ {name: "Serdar", age: 27}.to_json
+end
+
+Kemal.run
diff --git a/samples/websocket_server.cr b/samples/websocket_server.cr
new file mode 100644
index 0000000..61a0802
--- /dev/null
+++ b/samples/websocket_server.cr
@@ -0,0 +1,11 @@
+require "kemal"
+
+ws "/" do |socket|
+ socket.send "Hello from Kemal!"
+
+ socket.on_message do |message|
+ socket.send "Echo back from server #{message}"
+ end
+end
+
+Kemal.run
diff --git a/shard.yml b/shard.yml
index e23257b..d0371c4 100644
--- a/shard.yml
+++ b/shard.yml
@@ -1,5 +1,5 @@
name: kemal
-version: 1.10.1
+version: 1.6.0
authors:
- Serdar Dogruyol
@@ -15,7 +15,6 @@ dependencies:
development_dependencies:
ameba:
github: crystal-ameba/ameba
- branch: master
crystal: ">= 0.36.0"
diff --git a/spec/asset/hello_with_content_for.ecr b/spec/asset/hello_with_content_for.ecr
index 8b4dc69..b5460f9 100644
--- a/spec/asset/hello_with_content_for.ecr
+++ b/spec/asset/hello_with_content_for.ecr
@@ -2,4 +2,4 @@ Hello <%= name %>
<% content_for "meta" do %>
Kemal Spec
-<% end %>
+<% end %>
\ No newline at end of file
diff --git a/spec/asset/layout_with_yield.ecr b/spec/asset/layout_with_yield.ecr
index a025b2a..3710c4a 100644
--- a/spec/asset/layout_with_yield.ecr
+++ b/spec/asset/layout_with_yield.ecr
@@ -5,4 +5,4 @@
<%= content %>
-