Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .JuliaFormatter.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
indent = 4
margin = 80
always_for_in = true
whitespace_typedefs = true
whitespace_ops_in_indices = false
remove_extra_newlines = true
short_to_long_function_def = true
long_to_short_function_def = false
always_use_return = true
annotate_untyped_fields_with_any = true
conditional_to_if = true
normalize_line_endings = "unix"
trailing_comma = true
indent_submodule = true
separate_kwargs_with_semicolon = true
format_markdown = true

#? This breaks src/engee_front.jl docstrings. If true use `ignore` option.
# format_docstrings = true
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "julia"
directory: "/"
schedule:
interval: "weekly"
# groups: # uncomment to group all julia package updates into a single PR
# all-julia-packages:
# patterns:
# - "*"
70 changes: 70 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: CI

on:
push:
branches:
- '**'
paths:
- 'src/**'
- 'test/**'
- 'Project.toml'
pull_request:
paths:
- 'src/**'
- 'test/**'
- 'Project.toml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 60
permissions:
actions: write
contents: read
strategy:
fail-fast: false
matrix:
version:
- '1'
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: ${{ matrix.version }}
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1

coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
actions: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
with:
version: '1'
- uses: julia-actions/cache@v2
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
with:
coverage: true
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v5
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.env
.git-credentials
.vscode
Makefile.local

Manifest.toml
84 changes: 84 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Style

### Git

- Use conventional commit namings
- Write comprehensive, yet succint commit summaries that support commit name and diff, without utterly describing it

## Commands

```bash
make test # Run tests (failfast=true, verbose=true by default)
make format # Format all Julia files with JuliaFormatter
make repl # Start Julia REPL with project loaded
make clean # Remove tmp/ and test/tmp/
make init-dev # Initialize dev environment (needed for format)
make pre-commit-install # Install git pre-commit hooks
```

Test environment variables (override via env or `Makefile.local`):

- `JULIA_TEST_FAILFAST` — default `true`
- `JULIA_TEST_VERBOSE` — default `true`

Formatting is configured in `.JuliaFormatter.toml` (4-space indent, 80-char margin, trailing commas).

## Architecture

The package provides macro-driven HTTP server and client abstractions, eliminating boilerplate type definitions via metaprogramming.

**Module structure:**

- `SimpleHTTP` (entry: [src/SimpleHTTP.jl](src/SimpleHTTP.jl)) — re-exports `Server`, `Client`, `HTTP`, `ServerConfig`, `ClientConfig`, `@ip_str`, `IPAddr`
- `Common` ([src/Common.jl](src/Common.jl)) — shared utilities: `ParamData`, `ArgLoc` enum, JSON serialization (`write_json`/`read_json`), `parse_params`, `ErrorResponse`
- `Server` ([src/Server.jl](src/Server.jl)) — HTTP server submodule
- `Client` ([src/Client.jl](src/Client.jl)) — HTTP client submodule

**Core design — macro-driven route definitions:**

Both `Server` and `Client` expose `@get`, `@post`, `@put`, `@delete` macros that take a config, path, and function signature. At expansion time, they:

1. Parse the function signature's type annotations to determine how each argument maps to the request (`parse_params` in `Common`)
2. Generate a handler (server) or request function (client) with all parameter extraction/serialization wired in

**Parameter type annotation conventions** (declared in function signatures):

- Plain type (e.g., `::Int`) → query parameter (or URL param if `{argname}` appears in path)
- `Json{T}` → full request/response body deserialized as `T`
- `JsonField{T}` → individual field from a JSON body
- `Headers` → all request headers as `Dict{String,String}`
- `Headers["key"]` → specific header by name (lowercased)
- Default values (`= val`) make parameters optional

**Server macro signature:**

```julia
Server.@get(cfg, "/path/{id}", function route_name(id::String, q::Int = 0)::RetType
# body
end, error_codes_dict)
```

**Client macro signature:**

```julia
Client.@get(cfg, "/path/{id}", route_name(id::String, q::Int = 0)::RetType, error_codes_dict)
```

**Error handling:**

- Server: `error_codes` is a `Dict{Type,Int}` mapping exception types → HTTP status codes. Unmapped exceptions → 500 (body controlled by `verbosity_500`). Mapped exceptions are serialized via `Common.serialize`.
- Client: `err_map` is a `Dict{Int,Type}` mapping HTTP status codes → exception types. Unmapped non-2xx → `Client.UnexpectedResponseError`. Matched statuses → `deserialize(resp.body, type)`.

**Response codes:**

- `200` — successful response with body
- `204` — handler returns `Nothing`
- `422` — parameter/body parsing failure

**Serialization:** JSON3 with `allow_inf=true`. Error responses use `ErrorResponse` struct with a single `error::String` field.

**Tests** ([test/](test/)) use a live server (`Server.serve!`) with a `ServerTest` module defining actual routes and a `ClientTest` module defining the matching client calls, then exercise them end-to-end.
77 changes: 77 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
HELP_DESCRIPTION = "Various utilities for the current project."
JULIA_BIN ?= julia
JULIA_ARGS ?= --threads=6,2 --optimize=0
CLEAN_DIRS ?= tmp/ test/tmp/
JULIA_TEST_FAILFAST ?= true
JULIA_TEST_VERBOSE ?= true

.PHONY: all
all: help

-include Makefile.local

.PHONY: repl
repl: # Start Julia REPL in the current project
JULIA_PKG_PRECOMPILE_AUTO=0 $(JULIA_BIN) $(JULIA_ARGS) --project=.

.PHONY: test
test: # Run tests
@echo "Testing..."
JULIA_TEST_FAILFAST=$(JULIA_TEST_FAILFAST) \
JULIA_TEST_VERBOSE=$(JULIA_TEST_VERBOSE) \
$(JULIA_BIN) $(JULIA_ARGS) --compile=min --startup-file=no --project=. \
-e 'import Pkg; Pkg.test()'

.PHONY: clean
clean: # Clean project artifacts
$(foreach path,$(CLEAN_DIRS), \
rm -rf $(path); \
)

.PHONY: format
format: # Format all Julia files in the current directory
$(JULIA_BIN) $(JULIA_ARGS) --project=dev -e 'using JuliaFormatter; format(".")'

.PHONY: pre-commit-install
pre-commit-install: # Install pre-commit hooks
cp dev/scripts/pre-commit-hook.jl .git/hooks/pre-commit

.PHONY: pre-commit-uninstall
pre-commit-uninstall: # Uninstall pre-commit hooks
rm .git/hooks/pre-commit

.PHONY: bump-major
bump-major: # Increment project MAJOR version
$(JULIA_BIN) $(JULIA_ARGS) dev/scripts/bumpversion.jl Project.toml --major

.PHONY: bump-minor
bump-minor: # Increment project MINOR version
$(JULIA_BIN) $(JULIA_ARGS) dev/scripts/bumpversion.jl Project.toml --minor

.PHONY: bump-patch
bump-patch: # Increment project PATCH version
$(JULIA_BIN) $(JULIA_ARGS) dev/scripts/bumpversion.jl Project.toml --patch

.PHONY: bump-prerelease
bump-prerelease: # Increment project PRERELEASE version
$(JULIA_BIN) $(JULIA_ARGS) dev/scripts/bumpversion.jl Project.toml --prerelease

.PHONY: init-dev
init-dev: # Initialize dev environment
$(JULIA_BIN) $(JULIA_ARGS) --project=dev -e 'import Pkg; Pkg.instantiate()'

.PHONY: reset-dev
reset-dev: # Reset dev environment
$(JULIA_BIN) $(JULIA_ARGS) --project=dev \
-e 'rm("dev/Manifest.toml"; force=true); import Pkg; Pkg.instantiate()'

.PHONY: help
help:
ifdef HELP_DESCRIPTION
@printf "%s\n\n" $(HELP_DESCRIPTION) | fmt
endif
@echo "Available commands:"
@grep -vE '^[[:space:]]' $(MAKEFILE_LIST) | \
grep -E '^.*:.* #' | \
sed -E 's/(.*):(.*):.*#(.*)/ \2###\3/' | \
column -t -s '###'
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "SimpleHTTP"
uuid = "03cab53d-42c8-4ab5-8d7c-14ec844b19aa"
authors = ["Denis Revunov <rnovds@gmail.com>"]
version = "0.1.0"
authors = ["Denis Revunov <rnovds@gmail.com>"]

[deps]
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
Expand All @@ -16,9 +16,10 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat]
HTTP = "1.10.17"
JSON3 = "1.14.3"
MacroTools = "0.5.16"
MacroTools = "0.5.16 - 0"
Match = "2.4.0"
OrderedCollections = "1.8.1"
Sockets = "1.11.0"
Test = "1.11.0"
UUIDs = "1.11.0"
julia = "1.12"
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# SimpleHTTP.jl

A draft of a package to easily make and serve HTTP requests, without defining boilerplate types
6 changes: 6 additions & 0 deletions dev/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[deps]
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"

[compat]
JuliaFormatter = "1.0.62"
julia = "1.11"
50 changes: 50 additions & 0 deletions dev/scripts/bumpversion.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env -S julia --project=dev

import Pkg

function main(args)
@assert length(args) == 2 "pass `project_file` and patch `type`"
project_file = args[1]
type = args[2]

project = Pkg.Types.read_project("Project.toml")
old_version = project.version
new_version = if type == "--major"
Base.nextmajor(project.version)
elseif type == "--minor"
Base.nextminor(project.version)
elseif type == "--patch"
Base.nextpatch(project.version)
elseif type == "--prerelease"
nextprerelease(project.version)
else
throw(ArgumentError("type `$type` is not supported"))
end
project.version = new_version
Pkg.Types.write_project(project, project_file)
println(
"Bumped project version in $project_file from v$old_version to v$new_version",
)

return 0
end

function thisprerelease(v::VersionNumber)
return VersionNumber(v.major, v.minor, v.patch, v.prerelease)
end

function nextprerelease(v::VersionNumber)
return if v < thisprerelease(v)
thisprerelease(v)
else
@assert length(v.prerelease) == 2 "only prerelease versions like `rc.1` are supported"
VersionNumber(
v.major,
v.minor,
v.patch,
(v.prerelease[1], v.prerelease[2] + 1),
)
end
end

(abspath(PROGRAM_FILE) == @__FILE__) && exit(main(ARGS))
Loading