From a6b9524339fa018097688083f01a159ae16c48d5 Mon Sep 17 00:00:00 2001 From: KeviM Date: Fri, 25 Sep 2026 05:21:44 -0700 Subject: [PATCH 1/2] docs: publish an API reference; tighten clang-tidy, guides, and packaging - Doxyfile and `make docs` build the API reference from the public headers and user docs, with warnings as errors. docs.yml builds it on pull requests and deploys it to GitHub Pages on release tags. - clang-tidy now runs modernize-* plus targeted readability and C++ Core Guidelines checks; the config says why each exclusion stays out. - libwebsockets is found through its CMake package (vcpkg, Homebrew, Ubuntu), with pkg-config as the fallback. cmake/KalshiWebsockets.cmake keeps that package's directory-wide include_directories() from exposing libwebsockets' warnings, here and in consumers. - CONTRIBUTING is shorter and matches the tooling, SECURITY covers Ed25519, and README links the API reference. - The generator no longer cuts a long description inside a code span, and a summary that introduces a list stops before it. --- .clang-tidy | 35 +++++--- .github/workflows/docs.yml | 54 ++++++++++++ CHANGELOG.md | 2 + CMakeLists.txt | 10 +-- CONTRIBUTING.md | 155 +++++++++++++++-------------------- Doxyfile | 36 ++++++++ Makefile | 9 +- README.md | 13 ++- SECURITY.md | 37 +++------ cmake/KalshiWebsockets.cmake | 29 +++++++ cmake/kalshiConfig.cmake.in | 11 +-- docs/research.md | 3 +- include/kalshi/ws_models.hpp | 18 +--- tools/codegen/common.py | 9 +- 14 files changed, 262 insertions(+), 159 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 Doxyfile create mode 100644 cmake/KalshiWebsockets.cmake diff --git a/.clang-tidy b/.clang-tidy index e319898..f416303 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -3,6 +3,10 @@ # - performance-enum-size: narrowing public enums would change the ABI for little gain. # - optin.performance.Padding: public parameter structs are ordered for readability and # aggregate initialization, not layout. +# - modernize-use-trailing-return-type, modernize-avoid-c-arrays: style preferences this +# code base does not follow; C arrays appear only at C API boundaries. +# - modernize-use-nodiscard: [[nodiscard]] is placed deliberately on public results. +# - modernize-use-auto: the project spells out local types (tools/cpp_auto_audit.py). Checks: > -*, bugprone-*, @@ -12,19 +16,30 @@ Checks: > performance-*, -performance-enum-size, portability-*, - modernize-use-nullptr, - modernize-use-override, - modernize-use-equals-default, - modernize-use-equals-delete, - modernize-redundant-void-arg, - modernize-use-using, + modernize-*, + -modernize-use-trailing-return-type, + -modernize-avoid-c-arrays, + -modernize-use-nodiscard, + -modernize-use-auto, + readability-braces-around-statements, + readability-const-return-type, readability-container-size-empty, - readability-redundant-string-init, + readability-else-after-return, + readability-inconsistent-declaration-parameter-name, readability-make-member-function-const, - misc-unused-using-decls, + readability-misleading-indentation, + readability-redundant-control-flow, + readability-redundant-member-init, + readability-redundant-smartptr-get, + readability-redundant-string-init, + readability-simplify-boolean-expr, misc-definitions-in-headers, - cppcoreguidelines-virtual-class-destructor, - cppcoreguidelines-slicing + misc-unused-using-decls, + cppcoreguidelines-init-variables, + cppcoreguidelines-missing-std-forward, + cppcoreguidelines-prefer-member-initializer, + cppcoreguidelines-slicing, + cppcoreguidelines-virtual-class-destructor WarningsAsErrors: '*' HeaderFilterRegex: '.*/(include/kalshi(/detail)?|src(/api|/auth|/http|/models|/ws)?)/[^/]+$' FormatStyle: file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..5fd3dc0 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,54 @@ +name: Docs + +# Builds the Doxygen API reference on pull requests that touch it, and +# publishes it to GitHub Pages for each release tag. +on: + push: + tags: ["v*"] + pull_request: + paths: + - "include/**" + - "docs/**" + - "*.md" + - "Doxyfile" + - ".github/workflows/docs.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docs-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install Doxygen + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends doxygen + - name: Build the API reference + run: make docs + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + if: github.event_name != 'pull_request' + with: + path: build-docs/html + + deploy: + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index e68b9d5..b3f0d4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Added +- An API reference built from the headers with `make docs` and published to + for each release. - `WebSocketClient` covers every channel and command in Kalshi's AsyncAPI document: all 13 channels, `update_subscription` for markets, snapshots, CF Benchmarks indices, and Pyth underlyings, and `list_subscriptions`. Message diff --git a/CMakeLists.txt b/CMakeLists.txt index b5cb86a..5b6b8fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,14 +95,7 @@ configure_file( find_package(OpenSSL 3.0 REQUIRED) find_package(CURL REQUIRED) -if(WIN32) - find_package(libwebsockets CONFIG REQUIRED) - set(KALSHI_WEBSOCKETS_TARGET websockets_shared) -else() - find_package(PkgConfig REQUIRED) - pkg_check_modules(WEBSOCKETS REQUIRED IMPORTED_TARGET libwebsockets) - set(KALSHI_WEBSOCKETS_TARGET PkgConfig::WEBSOCKETS) -endif() +include(cmake/KalshiWebsockets.cmake) # Glaze is a private, header-only build dependency; installed headers never # include it. src/json.hpp rejects versions older than 8.3 at compile time, @@ -178,6 +171,7 @@ install(EXPORT kalshiTargets NAMESPACE kalshi:: DESTINATION ${KALSHI_INSTALL_CMAKEDIR}) install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/KalshiWebsockets.cmake ${CMAKE_CURRENT_BINARY_DIR}/kalshiConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/kalshiConfigVersion.cmake DESTINATION ${KALSHI_INSTALL_CMAKEDIR}) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a825cd9..d9c21ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,113 +1,92 @@ -# Contributing to kalshi-cpp +# Contributing -Thanks for your interest in contributing. This guide covers the local -build flow, code style expectations, and PR conventions used in this -repo. For security-vulnerability reports, see [SECURITY.md](SECURITY.md) -and do not open a public issue for those. +Report security problems privately as described in [SECURITY.md](SECURITY.md), +not in a public issue. -## Getting started +## Set up ```bash git clone https://github.com/Reddimus/kalshi-cpp.git cd kalshi-cpp -# One-time: install build deps (Ubuntu 24.04 example) -sudo apt install -y build-essential cmake clang-format \ +# Ubuntu 24.04. Its apt CMake is older than the 3.31 this build needs. +sudo apt install build-essential ninja-build pkg-config clang-format-18 python3-yaml \ libssl-dev libcurl4-openssl-dev libwebsockets-dev +pipx install cmake -make build # CMake configure + Release build -make test # Run unit tests (ctest) +make test ``` -macOS uses Homebrew (`brew install openssl curl libwebsockets`), -Windows uses vcpkg (the CI workflow has the exact invocations). +On macOS, run `brew install cmake ninja pkg-config openssl libwebsockets llvm@18` +and `python3 -m pip install pyyaml`. Windows builds use vcpkg; the +`build-windows` job in `.github/workflows/ci.yml` has the steps. -## Development workflow +## Everyday commands ```bash -make debug # Debug build in build-debug/ -make test # Build and run the tests -make sanitize # ASan + UBSan -make tsan # ThreadSanitizer -make tidy # clang-tidy build -make lint # clang-format 18, cpp_auto_audit, generated-code check -make codegen # Regenerate the REST client from spec/openapi.yaml -make format # Apply clang-format in place -make bench # Google Benchmark suite -make coverage # lcov report (needs lcov) -make clean # Remove build directories +make test # Release build and tests +make debug # Debug build in build-debug/ +make sanitize tsan # ASan + UBSan, ThreadSanitizer +make tidy # clang-tidy build +make lint # clang-format 18, explicit-type audit, generated-code check +make format # Apply clang-format +make codegen # Regenerate from spec/ +make docs # Doxygen API reference in build-docs/html (needs Doxygen) +make bench # Google Benchmark suite +make consumers # Build installed and FetchContent consumers +make coverage # lcov report (needs lcov) ``` -Run `make lint` before pushing; CI runs the same checks. It needs -clang-format 18 and PyYAML (`python3 -m pip install pyyaml`). +Run `make format lint test` before pushing. `make install-hooks` runs format +and lint on every commit. CI adds the sanitizers, clang-tidy, and the consumer +check, on Linux, macOS, and Windows. ## Generated code -`tools/codegen/generate.py` writes the REST client from `spec/openapi.yaml` -and the WebSocket messages from `spec/asyncapi.yaml`: `include/kalshi/api.hpp`, -`models.hpp`, and `ws_models.hpp`, `src/api/operations/`, `src/api/validate.hpp`, -`src/models/json_meta.hpp`, `src/ws/wire.hpp`, `tests/test_operation_routes.cpp`, -`tests/test_ws_messages.cpp`, `docs/operations.md`, and `docs/channels.md`. -Change the generator or a spec, run `make codegen`, and commit both. CI rejects stale -output. `docs/research.md` explains how to refresh the spec. +`tools/codegen/generate.py` writes the REST client from `spec/openapi.yaml` and +the WebSocket types from `spec/asyncapi.yaml`: `include/kalshi/api.hpp`, +`models.hpp`, and `ws_models.hpp`, `src/api/operations/`, +`src/api/validate.hpp`, `src/models/json_meta.hpp`, `src/ws/wire.hpp`, +`tests/test_operation_routes.cpp`, `tests/test_ws_messages.cpp`, +`docs/operations.md`, and `docs/channels.md`. Change the generator, a template +in `tools/codegen/`, or a spec, run `make codegen`, and commit the result. +`make lint` fails on stale output. [docs/research.md](docs/research.md) +explains how to refresh the specs. ## Code style -- **C++23** features encouraged: `std::expected` for all - error-returning operations, no exceptions in the public API. -- **No `auto`** for local variable declarations. Spell out the type so - reviewers can verify intent without IDE help. Carve-outs: - - Structured bindings: `auto& [k, v] = ...` - - Lambda closures: `auto callback = ...` - - Iterator-like results: `auto it = container.find(...)` -- **Formatting**: `.clang-format` (LLVM base, tabs, 100-col limit). - `make format` applies it. -- **Includes**: project headers first, then system headers - (enforced by clang-format `SortIncludes`). -- **JSON**: use Glaze for structured payloads. Keep hand-rolled scanners limited - to measured hot paths with focused parser and benchmark tests. - -## PR conventions - -- Branch names: `feat/...`, `fix/...`, `docs/...`, `chore/...`, - `test/...`, `build/...`, `ci/...`, `refactor/...`. -- Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/): - `(): `, for example: - `fix(ws): null-guard moved-from accessors`. -- Squash + delete branch on merge. PR titles become the squash commit - subject, so write them clearly. -- Update `CHANGELOG.md` under `## [Unreleased]` for any user-visible - change (new API, fix that consumers will notice, dep bump). Use the - Keep-a-Changelog sub-headers: Added / Changed / Fixed / Removed. -- CI must pass on all platforms (Ubuntu 24.04 + macOS + Windows) before - merge. - -## Release process - -Releases are cut from `main` via tag push: - -```bash -# 1. Update CMakeLists.txt VERSION and move CHANGELOG.md entries into [X.Y.Z]. -# release.yml refuses tags without a matching CHANGELOG section or passing CI. -# 2. Commit the version bump -git commit -am "chore(release): cut vX.Y.Z" -git push origin main - -# 3. Tag and push the tag; release.yml creates the GitHub Release -git tag vX.Y.Z -git push origin vX.Y.Z -``` - -Semver: bump MINOR for new public API, PATCH for fixes/docs/CI. - -## Reporting issues - -- **Bugs / feature requests**: open a GitHub issue with reproduction - steps + the kalshi-cpp version (`kalshi::VERSION`). -- **Security vulnerabilities**: see [SECURITY.md](SECURITY.md) for the - private reporting channel. +- Public functions return `kalshi::Result` (`std::expected`) and + don't throw. +- Spell out local variable types. `auto` is fine for structured bindings, + lambdas, and iterators named `it` or `iter`; mark anything else with a + `// auto-ok: reason` comment. `make lint` checks this. +- `.clang-format` sets the layout: tabs, 100 columns, project includes before + system includes. +- Glaze reads and writes JSON. Don't add hand-written JSON scanners. +- Keep tests offline. Inject an `HttpTransport`, or use the local HTTP and + WebSocket servers in `tests/`. + +## Pull requests + +- Name branches `feat/`, `fix/`, `docs/`, `ci/`, `refactor/`, `test/`, or + `chore/`. +- Title PRs as [Conventional Commits](https://www.conventionalcommits.org/), + such as `fix(ws): keep subscriptions across reconnects`. PRs are + squash-merged, so the title becomes the commit subject. +- Note user-visible changes in `CHANGELOG.md` under `[Unreleased]`. + +## Releases + +1. Set `VERSION` in `CMakeLists.txt`, move the `[Unreleased]` notes into a new + `[X.Y.Z]` section, and update the `GIT_TAG` in `README.md`. +2. Merge that change to `main`. +3. Run `git tag vX.Y.Z && git push origin vX.Y.Z`. `release.yml` publishes the + GitHub release after CI passes on the tagged commit, and `docs.yml` + publishes the API reference. + +While the version is 0.x, a minor release may break the API; patch releases +only fix things. ## License -By contributing, you agree your changes are licensed under the MIT -license that covers this repository. +Contributions are licensed under the repository's MIT license. diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..d4c3e6b --- /dev/null +++ b/Doxyfile @@ -0,0 +1,36 @@ +# API reference for GitHub Pages. `make docs` writes it to build-docs/html. +# Only settings that differ from Doxygen's defaults are listed. + +PROJECT_NAME = kalshi-cpp +PROJECT_NUMBER = $(KALSHI_VERSION) +PROJECT_BRIEF = "C++23 client for Kalshi's Predictions API" +OUTPUT_DIRECTORY = build-docs + +INPUT = README.md \ + docs/operations.md \ + docs/channels.md \ + docs/api-coverage.md \ + docs/research.md \ + CHANGELOG.md \ + CONTRIBUTING.md \ + SECURITY.md \ + include/kalshi +FILE_PATTERNS = *.hpp *.md +EXCLUDE = include/kalshi/detail +USE_MDFILE_AS_MAINPAGE = README.md +STRIP_FROM_PATH = include +STRIP_FROM_INC_PATH = include + +# Generated models document their fields from the spec where it does, so list +# every member rather than hiding undocumented ones. +EXTRACT_ALL = YES +JAVADOC_AUTOBRIEF = YES +BUILTIN_STL_SUPPORT = YES +SORT_MEMBER_DOCS = NO +WARN_IF_UNDOCUMENTED = NO +WARN_AS_ERROR = FAIL_ON_WARNINGS +QUIET = YES + +GENERATE_LATEX = NO +GENERATE_TREEVIEW = YES +HAVE_DOT = NO diff --git a/Makefile b/Makefile index 12abafd..ec4f842 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ CLANG_FORMAT_MAJOR := 18 CPP_SOURCES = git ls-files -z --cached --others --exclude-standard '*.cpp' '*.hpp' | \ xargs -0 sh -c 'for f; do [ -e "$$f" ] && printf "%s\0" "$$f"; done' _ -.PHONY: all configure build debug test sanitize tsan tidy bench consumers codegen lint lint-docs \ +.PHONY: all configure build debug test sanitize tsan tidy bench consumers codegen docs lint lint-docs \ format pre-commit install-hooks coverage clean help all: build @@ -48,6 +48,10 @@ bench: consumers: ./tools/test_consumers.sh +# Needs Doxygen. Opens at build-docs/html/index.html. +docs: + KALSHI_VERSION=$$(./tools/project_version.sh) doxygen Doxyfile + # Needs PyYAML and clang-format 18. codegen: CLANG_FORMAT="$(CLANG_FORMAT)" $(PYTHON) tools/codegen/generate.py @@ -103,7 +107,8 @@ help: @echo "make tidy clang-tidy build in build-tidy/" @echo "make bench Google Benchmark suite in build-bench/ (BENCH_ARGS=...)" @echo "make consumers Check install and FetchContent consumers" - @echo "make codegen Regenerate the REST client from spec/openapi.yaml" + @echo "make codegen Regenerate the REST client and WebSocket types from spec/" + @echo "make docs Doxygen API reference in build-docs/html" @echo "make lint clang-format, explicit-type audit, generated-code check" @echo "make lint-docs markdownlint" @echo "make format Format C++ sources in place" diff --git a/README.md b/README.md index c2e9fac..b8d18a1 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,17 @@ Kalshi's OpenAPI document and streams market data over WebSockets. Requests are signed with Ed25519 or RSA-PSS keys, prices stay exact fixed-point strings, and every call returns `std::expected` instead of throwing. -The REST client is generated from [`spec/openapi.yaml`](spec/openapi.yaml), so -model and field names match [Kalshi's API reference](https://docs.kalshi.com). -Kalshi's separate Margin and Perpetuals API is out of scope. +The client is generated from Kalshi's OpenAPI and AsyncAPI documents in +[`spec/`](spec/), so type and field names match +[Kalshi's API reference](https://docs.kalshi.com). The +[API reference for this library](https://reddimus.github.io/kalshi-cpp/) is +built from its headers. Kalshi's separate Margin and Perpetuals API is out of +scope. ## Build You need a C++23 compiler, CMake 3.31+, OpenSSL 3, libcurl, and libwebsockets. +Ubuntu 24.04's apt CMake is older; `pipx install cmake` gets a current one. ```bash brew install cmake openssl curl libwebsockets pkg-config # macOS @@ -168,7 +172,8 @@ in `.env`, then run `make run-portfolio`. make format lint test # before every commit make sanitize tsan tidy # ASan/UBSan, ThreadSanitizer, clang-tidy make consumers bench # packaging check, benchmarks -make codegen # after updating spec/openapi.yaml +make codegen # after updating a spec in spec/ +make docs # API reference in build-docs/html ``` [CONTRIBUTING.md](CONTRIBUTING.md) covers the workflow and release steps. diff --git a/SECURITY.md b/SECURITY.md index e569d9e..779787a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,43 +1,30 @@ # Security policy -`kalshi-cpp` is a third-party C++ client for the Kalshi exchange API. -It signs every request with an account-bound RSA private key, so a -vulnerability that mishandles credentials or leaks request material -could put live trading capital at risk. This file is the canonical -contact path for reporting one. +`kalshi-cpp` is a third-party C++ client for the Kalshi exchange API. It signs +requests with an account's Ed25519 or RSA private key, so a bug that leaks a +key or lets someone forge requests could put trading capital at risk. ## Supported versions -Security fixes are made on the latest published `vX.Y.Z` tag. Older -tags are not back-patched. Bump your `FetchContent_Declare(... GIT_TAG ...)` -pin or your `find_package(kalshi X.Y.Z CONFIG REQUIRED)` constraint to -the latest minor on the same major as part of the upgrade. - -| Version | Supported | -| ---------- | ------------------ | -| latest tag | :white_check_mark: | -| older | :x: | +Fixes go into the next release after the latest tag; older releases are not +patched. Upgrade by moving your `GIT_TAG` or `find_package(kalshi X.Y CONFIG)` +version to the new release. ## Reporting a vulnerability -**Do not open a public issue.** Use GitHub's [private vulnerability -reporting](https://github.com/Reddimus/kalshi-cpp/security/advisories/new) -flow, which delivers the report to the maintainer privately and -tracks coordinated disclosure. +Don't open a public issue. Use GitHub's [private vulnerability +reporting](https://github.com/Reddimus/kalshi-cpp/security/advisories/new), +which reaches the maintainer privately and tracks disclosure. -When reporting, please include: +Include: - Affected version (tag or commit SHA) - A minimal reproduction or test case - Impact (credential leak / request forgery / DoS / something else) - Whether you've notified anyone else (e.g. Kalshi directly) -You can expect: - -- Acknowledgement within **3 business days** -- An initial assessment + severity rating within **7 business days** -- A fix on a new `vX.Y.Z+1` tag, or a clear timeline if the fix is - larger +You should hear back within 3 business days, and get an assessment within 7. +The fix ships in a new release, or you get a timeline if it takes longer. ## Out of scope diff --git a/cmake/KalshiWebsockets.cmake b/cmake/KalshiWebsockets.cmake new file mode 100644 index 0000000..bccb761 --- /dev/null +++ b/cmake/KalshiWebsockets.cmake @@ -0,0 +1,29 @@ +# Finds libwebsockets and sets KALSHI_WEBSOCKETS_TARGET to an imported target. +# Used by kalshi-cpp's build and by its installed package config. +# +# libwebsockets' own CMake package (vcpkg, Homebrew, and most distributions ship +# one) adds its headers with a directory-wide include_directories(), which +# exposes their warnings to every target in the caller's directory. This moves +# them onto the imported target, where they count as system headers. Without +# that package, pkg-config is used. + +find_package(libwebsockets CONFIG QUIET) +if(TARGET websockets_shared OR TARGET websockets) + if(TARGET websockets_shared) + set(KALSHI_WEBSOCKETS_TARGET websockets_shared) + else() + set(KALSHI_WEBSOCKETS_TARGET websockets) + endif() + if(DEFINED LIBWEBSOCKETS_INCLUDE_DIRS) + get_property(_kalshi_include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES) + list(REMOVE_ITEM _kalshi_include_dirs ${LIBWEBSOCKETS_INCLUDE_DIRS}) + set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "${_kalshi_include_dirs}") + set_property(TARGET ${KALSHI_WEBSOCKETS_TARGET} APPEND PROPERTY + INTERFACE_INCLUDE_DIRECTORIES ${LIBWEBSOCKETS_INCLUDE_DIRS}) + unset(_kalshi_include_dirs) + endif() +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(WEBSOCKETS REQUIRED IMPORTED_TARGET libwebsockets) + set(KALSHI_WEBSOCKETS_TARGET PkgConfig::WEBSOCKETS) +endif() diff --git a/cmake/kalshiConfig.cmake.in b/cmake/kalshiConfig.cmake.in index 2e98379..15bb27b 100644 --- a/cmake/kalshiConfig.cmake.in +++ b/cmake/kalshiConfig.cmake.in @@ -4,11 +4,12 @@ include(CMakeFindDependencyMacro) find_dependency(OpenSSL 3.0) find_dependency(CURL) -if(NOT WIN32) - find_dependency(PkgConfig) - pkg_check_modules(WEBSOCKETS REQUIRED IMPORTED_TARGET libwebsockets) -else() - find_dependency(libwebsockets CONFIG) +include("${CMAKE_CURRENT_LIST_DIR}/KalshiWebsockets.cmake") +# The exported targets link the libwebsockets target this package was built +# against; map it to whichever one was found here. +if(NOT TARGET @KALSHI_WEBSOCKETS_TARGET@) + add_library(@KALSHI_WEBSOCKETS_TARGET@ INTERFACE IMPORTED) + target_link_libraries(@KALSHI_WEBSOCKETS_TARGET@ INTERFACE ${KALSHI_WEBSOCKETS_TARGET}) endif() include("${CMAKE_CURRENT_LIST_DIR}/kalshiTargets.cmake") diff --git a/docs/research.md b/docs/research.md index 0b0898e..bdecb28 100644 --- a/docs/research.md +++ b/docs/research.md @@ -26,8 +26,7 @@ RSA-PSS and SHA-256; Ed25519 keys sign the message directly. See Kalshi's curl -fsS https://docs.kalshi.com/openapi.yaml -o spec/openapi.yaml curl -fsS https://docs.kalshi.com/asyncapi.yaml -o spec/asyncapi.yaml shasum -a 256 spec/*.yaml -python3 tools/codegen/generate.py -make test +make codegen test ``` Review the diff of the generated files, update the table above, and note diff --git a/include/kalshi/ws_models.hpp b/include/kalshi/ws_models.hpp index 89b0ed2..46d6507 100644 --- a/include/kalshi/ws_models.hpp +++ b/include/kalshi/ws_models.hpp @@ -174,10 +174,7 @@ enum class LifecyclePriceLevelStructure : std::uint8_t { return ""; } -/// Field to annotate which of the event type this event is for: - `created` - Market created - -/// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated` - -/// Market close date updated - `determined` - Market determined - `settled` - Market settled - -/// `price_level_structu... +/// Field to annotate which of the event type this event is for enum class MarketLifecycleV2EventType : std::uint8_t { Unknown, ///< A value this SDK version does not know. Created, ///< `created` @@ -211,9 +208,7 @@ enum class MarketLifecycleV2EventType : std::uint8_t { return ""; } -/// Field to annotate which of the event type this event is for: - `created` - Market created - -/// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated` - -/// Market close date updated - `determined` - Market determined - `settled` - Market settled +/// Field to annotate which of the event type this event is for enum class MultivariateMarketLifecycleEventType : std::uint8_t { Unknown, ///< A value this SDK version does not know. Created, ///< `created` @@ -555,10 +550,7 @@ struct MarketLifecycleV2 { std::optional is_deactivated; /// Optional - This key will be emitted when the market is created std::optional additional_metadata; - /// Field to annotate which of the event type this event is for: - `created` - Market created - - /// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated` - /// - Market close date updated - `determined` - Market determined - `settled` - Market settled - /// - `price_level_structu... + /// Field to annotate which of the event type this event is for MarketLifecycleV2EventType event_type{}; /// Optional - The market price level structure on creation or price_level_structure_updated /// events @@ -631,9 +623,7 @@ struct MultivariateMarketLifecycle { std::optional is_deactivated; /// Optional - This key will be emitted when the market is created std::optional additional_metadata; - /// Field to annotate which of the event type this event is for: - `created` - Market created - - /// `activated` - Market activated - `deactivated` - Market deactivated - `close_date_updated` - /// - Market close date updated - `determined` - Market determined - `settled` - Market settled + /// Field to annotate which of the event type this event is for MultivariateMarketLifecycleEventType event_type{}; /// Optional - The market price level structure on creation std::optional price_level_structure; diff --git a/tools/codegen/common.py b/tools/codegen/common.py index 2554a0b..405ffe8 100644 --- a/tools/codegen/common.py +++ b/tools/codegen/common.py @@ -56,10 +56,17 @@ def first_sentence(text: str | None) -> str: return "" text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # markdown links -> text text = re.sub(r"<[^>]+>", "", text) + # A summary that introduces a list ends where the list starts. + text = re.split(r":\s*\n\s*[-*] ", text, maxsplit=1)[0] text = " ".join(text.split()) match = re.match(r"(.+?[.!?])(\s|$)", text) sentence = match.group(1) if match else text - return sentence if len(sentence) <= 300 else sentence[:297].rstrip() + "..." + if len(sentence) <= 300: + return sentence + cut = sentence[:297].rsplit(" ", 1)[0] + if cut.count("`") % 2: # never end inside a code span + cut = cut[:cut.rindex("`")].rstrip() + return cut + "..." def doc_lines(text: str, indent: str, width: int = 96) -> list[str]: From fabd34442c1f25f8eede42c8185b726a1aef2c50 Mon Sep 17 00:00:00 2001 From: KeviM Date: Fri, 25 Sep 2026 05:37:12 -0700 Subject: [PATCH 2/2] fix: address review findings on docs publishing, packaging, and guides - Deploy the API reference only when release.yml dispatches it after a release, one deploy at a time. - Run libwebsockets' CMake package inside a function and restore the directory's include and link directories exactly, instead of editing them; the installed config maps targets and reports not-found gracefully. - Link README's spec and example files absolutely so they work on Pages. - Fix the macOS and Ubuntu setup steps and align CONTRIBUTING with the audit. - List clang-tidy 18's modernize checks by name. - Keep the text when truncating a description that starts with a code span. --- .clang-tidy | 47 +++++++++++++++++++++------ .github/workflows/docs.yml | 17 ++++++---- .github/workflows/release.yml | 7 ++++ .gitignore | 3 ++ CHANGELOG.md | 4 +++ CMakeLists.txt | 1 + CONTRIBUTING.md | 32 ++++++++++++------ README.md | 10 +++--- cmake/KalshiWebsockets.cmake | 61 +++++++++++++++++++++-------------- cmake/kalshiConfig.cmake.in | 7 ++++ tools/codegen/common.py | 8 +++-- 11 files changed, 139 insertions(+), 58 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index f416303..28173cf 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -3,10 +3,11 @@ # - performance-enum-size: narrowing public enums would change the ABI for little gain. # - optin.performance.Padding: public parameter structs are ordered for readability and # aggregate initialization, not layout. -# - modernize-use-trailing-return-type, modernize-avoid-c-arrays: style preferences this -# code base does not follow; C arrays appear only at C API boundaries. -# - modernize-use-nodiscard: [[nodiscard]] is placed deliberately on public results. -# - modernize-use-auto: the project spells out local types (tools/cpp_auto_audit.py). +# - modernize: the clang-tidy 18 checks are listed by name, so a newer clang-tidy cannot add +# errors CI never sees. Left out: use-trailing-return-type and avoid-c-arrays (style this +# code base does not follow; C arrays appear only at C API boundaries), use-nodiscard +# ([[nodiscard]] is placed deliberately), and use-auto (local types are spelled out; see +# tools/cpp_auto_audit.py). Checks: > -*, bugprone-*, @@ -16,11 +17,39 @@ Checks: > performance-*, -performance-enum-size, portability-*, - modernize-*, - -modernize-use-trailing-return-type, - -modernize-avoid-c-arrays, - -modernize-use-nodiscard, - -modernize-use-auto, + modernize-avoid-bind, + modernize-concat-nested-namespaces, + modernize-deprecated-headers, + modernize-deprecated-ios-base-aliases, + modernize-loop-convert, + modernize-macro-to-enum, + modernize-make-shared, + modernize-make-unique, + modernize-pass-by-value, + modernize-raw-string-literal, + modernize-redundant-void-arg, + modernize-replace-auto-ptr, + modernize-replace-disallow-copy-and-assign-macro, + modernize-replace-random-shuffle, + modernize-return-braced-init-list, + modernize-shrink-to-fit, + modernize-type-traits, + modernize-unary-static-assert, + modernize-use-bool-literals, + modernize-use-constraints, + modernize-use-default-member-init, + modernize-use-emplace, + modernize-use-equals-default, + modernize-use-equals-delete, + modernize-use-noexcept, + modernize-use-nullptr, + modernize-use-override, + modernize-use-starts-ends-with, + modernize-use-std-numbers, + modernize-use-std-print, + modernize-use-transparent-functors, + modernize-use-uncaught-exceptions, + modernize-use-using, readability-braces-around-statements, readability-const-return-type, readability-container-size-empty, diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5fd3dc0..e70390c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,10 +1,9 @@ name: Docs -# Builds the Doxygen API reference on pull requests that touch it, and -# publishes it to GitHub Pages for each release tag. +# Builds the Doxygen API reference on pull requests that touch it. Deploys it +# to GitHub Pages when dispatched, which release.yml does after it publishes a +# release, so only released headers are documented. on: - push: - tags: ["v*"] pull_request: paths: - "include/**" @@ -19,7 +18,7 @@ permissions: concurrency: group: docs-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: build: @@ -34,13 +33,17 @@ jobs: - name: Build the API reference run: make docs - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 - if: github.event_name != 'pull_request' + if: github.event_name == 'workflow_dispatch' with: path: build-docs/html deploy: - if: github.event_name != 'pull_request' + if: github.event_name == 'workflow_dispatch' needs: build + # One deploy at a time, in order, so an older release never lands last. + concurrency: + group: pages-deploy + cancel-in-progress: false runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05bc40e..c3320ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,3 +73,10 @@ jobs: run: >- gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/notes.md" --verify-tag + + # Events from this workflow's token start no other workflows, except a + # dispatch, so the docs deploy is requested explicitly. + - name: Publish the API reference + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run docs.yml --repo "$GITHUB_REPOSITORY" --ref "$GITHUB_REF_NAME" diff --git a/.gitignore b/.gitignore index acbe64d..ea9757f 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ __pycache__/ *~ .DS_Store Thumbs.db + +# Local Python environment for tools/ +.venv/ diff --git a/CHANGELOG.md b/CHANGELOG.md index b3f0d4e..4a7254c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,10 @@ uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Changed +- libwebsockets is found through its own CMake package when one is installed + (vcpkg, Homebrew, and Ubuntu ship one), with pkg-config as the fallback on + every platform. The installed `kalshiConfig.cmake` finds it the same way and + reports kalshi-cpp as not found, instead of stopping, when it is missing. - CMake 3.31 or newer is required when Glaze is fetched (Glaze's own minimum), or 3.21 with `KALSHI_USE_SYSTEM_GLAZE=ON`. Glaze 8.3 or newer is enforced at compile time. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b6b8fd..135f0da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,7 @@ configure_file( find_package(OpenSSL 3.0 REQUIRED) find_package(CURL REQUIRED) include(cmake/KalshiWebsockets.cmake) +kalshi_find_websockets(TRUE) # Glaze is a private, header-only build dependency; installed headers never # include it. src/json.hpp rejects versions older than 8.3 at compile time, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9c21ce..381a2f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,18 +8,29 @@ not in a public issue. ```bash git clone https://github.com/Reddimus/kalshi-cpp.git cd kalshi-cpp +``` -# Ubuntu 24.04. Its apt CMake is older than the 3.31 this build needs. -sudo apt install build-essential ninja-build pkg-config clang-format-18 python3-yaml \ - libssl-dev libcurl4-openssl-dev libwebsockets-dev -pipx install cmake +On Ubuntu 24.04, whose apt CMake is older than the 3.31 this build needs: +```bash +sudo apt install build-essential ninja-build pkg-config clang-format-18 python3-yaml \ + pipx libssl-dev libcurl4-openssl-dev libwebsockets-dev +pipx install cmake && pipx ensurepath # then open a new shell make test ``` -On macOS, run `brew install cmake ninja pkg-config openssl libwebsockets llvm@18` -and `python3 -m pip install pyyaml`. Windows builds use vcpkg; the -`build-windows` job in `.github/workflows/ci.yml` has the steps. +On macOS, Homebrew's `llvm@18` provides clang-format 18 without putting it on +`PATH`, and PyYAML goes in a virtual environment: + +```bash +brew install cmake ninja pkg-config openssl libwebsockets llvm@18 +python3 -m venv .venv && .venv/bin/pip install pyyaml +export CLANG_FORMAT="$(brew --prefix llvm@18)/bin/clang-format" PYTHON=.venv/bin/python +make test lint +``` + +Windows builds use vcpkg; the `build-windows` job in `.github/workflows/ci.yml` +has the steps. ## Everyday commands @@ -58,11 +69,12 @@ explains how to refresh the specs. - Public functions return `kalshi::Result` (`std::expected`) and don't throw. - Spell out local variable types. `auto` is fine for structured bindings, - lambdas, and iterators named `it` or `iter`; mark anything else with a - `// auto-ok: reason` comment. `make lint` checks this. + lambdas, and iterators. Anything else needs an `// auto-ok: reason` comment + or an entry in `tools/cpp_auto_allowlist.txt`; `make lint` checks this. - `.clang-format` sets the layout: tabs, 100 columns, project includes before system includes. -- Glaze reads and writes JSON. Don't add hand-written JSON scanners. +- Glaze reads and writes JSON. `strip_null_members` is the only hand-written + scanner, and it runs only after a parse fails. - Keep tests offline. Inject an `HttpTransport`, or use the local HTTP and WebSocket servers in `tests/`. diff --git a/README.md b/README.md index b8d18a1..9bf9a7d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ signed with Ed25519 or RSA-PSS keys, prices stay exact fixed-point strings, and every call returns `std::expected` instead of throwing. The client is generated from Kalshi's OpenAPI and AsyncAPI documents in -[`spec/`](spec/), so type and field names match +[`spec/`](https://github.com/Reddimus/kalshi-cpp/tree/main/spec), so type and field names match [Kalshi's API reference](https://docs.kalshi.com). The [API reference for this library](https://reddimus.github.io/kalshi-cpp/) is built from its headers. Kalshi's separate Margin and Perpetuals API is out of @@ -158,10 +158,10 @@ lists each channel's message types. | Program | What it does | | --- | --- | -| [`market_data`](examples/market_data.cpp) | Markets, an order book, and candlesticks, without a key | -| [`portfolio`](examples/portfolio.cpp) | Balance, positions, and resting orders | -| [`place_and_cancel_order`](examples/place_and_cancel_order.cpp) | A resting order and its cancel, on the demo exchange only | -| [`stream_orderbook`](examples/stream_orderbook.cpp) | A live local order book that recovers from gaps and reconnects | +| [`market_data`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/market_data.cpp) | Markets, an order book, and candlesticks, without a key | +| [`portfolio`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/portfolio.cpp) | Balance, positions, and resting orders | +| [`place_and_cancel_order`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/place_and_cancel_order.cpp) | A resting order and its cancel, on the demo exchange only | +| [`stream_orderbook`](https://github.com/Reddimus/kalshi-cpp/blob/main/examples/stream_orderbook.cpp) | A live local order book that recovers from gaps and reconnects | Put `KALSHI_API_KEY_ID`, `KALSHI_API_KEY_FILE`, and optionally `KALSHI_ENV=demo` in `.env`, then run `make run-portfolio`. diff --git a/cmake/KalshiWebsockets.cmake b/cmake/KalshiWebsockets.cmake index bccb761..2a7449e 100644 --- a/cmake/KalshiWebsockets.cmake +++ b/cmake/KalshiWebsockets.cmake @@ -1,29 +1,42 @@ -# Finds libwebsockets and sets KALSHI_WEBSOCKETS_TARGET to an imported target. -# Used by kalshi-cpp's build and by its installed package config. +# kalshi_find_websockets() +# +# Finds libwebsockets and sets KALSHI_WEBSOCKETS_TARGET to an imported target +# and KALSHI_WEBSOCKETS_VIA to "cmake" or "pkg-config" in the caller's scope. # # libwebsockets' own CMake package (vcpkg, Homebrew, and most distributions ship -# one) adds its headers with a directory-wide include_directories(), which -# exposes their warnings to every target in the caller's directory. This moves -# them onto the imported target, where they count as system headers. Without -# that package, pkg-config is used. +# one) calls include_directories() and link_directories() and appends to +# CMAKE_MODULE_PATH and CMAKE_REQUIRED_INCLUDES. Running it inside a function +# drops the variable changes, and the directory properties are restored +# exactly, so nothing leaks into the caller. The headers go on the imported +# target instead, where they count as system headers. +function(kalshi_find_websockets required) + get_property(include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES) + get_property(link_dirs DIRECTORY PROPERTY LINK_DIRECTORIES) + find_package(libwebsockets CONFIG QUIET) + set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "${include_dirs}") + set_property(DIRECTORY PROPERTY LINK_DIRECTORIES "${link_dirs}") + + foreach(candidate websockets_shared websockets) + if(TARGET ${candidate}) + if(DEFINED LIBWEBSOCKETS_INCLUDE_DIRS) + set_property(TARGET ${candidate} APPEND PROPERTY + INTERFACE_INCLUDE_DIRECTORIES ${LIBWEBSOCKETS_INCLUDE_DIRS}) + endif() + set(KALSHI_WEBSOCKETS_TARGET ${candidate} PARENT_SCOPE) + set(KALSHI_WEBSOCKETS_VIA cmake PARENT_SCOPE) + return() + endif() + endforeach() -find_package(libwebsockets CONFIG QUIET) -if(TARGET websockets_shared OR TARGET websockets) - if(TARGET websockets_shared) - set(KALSHI_WEBSOCKETS_TARGET websockets_shared) - else() - set(KALSHI_WEBSOCKETS_TARGET websockets) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(WEBSOCKETS QUIET IMPORTED_TARGET GLOBAL libwebsockets) endif() - if(DEFINED LIBWEBSOCKETS_INCLUDE_DIRS) - get_property(_kalshi_include_dirs DIRECTORY PROPERTY INCLUDE_DIRECTORIES) - list(REMOVE_ITEM _kalshi_include_dirs ${LIBWEBSOCKETS_INCLUDE_DIRS}) - set_property(DIRECTORY PROPERTY INCLUDE_DIRECTORIES "${_kalshi_include_dirs}") - set_property(TARGET ${KALSHI_WEBSOCKETS_TARGET} APPEND PROPERTY - INTERFACE_INCLUDE_DIRECTORIES ${LIBWEBSOCKETS_INCLUDE_DIRS}) - unset(_kalshi_include_dirs) + if(TARGET PkgConfig::WEBSOCKETS) + set(KALSHI_WEBSOCKETS_TARGET PkgConfig::WEBSOCKETS PARENT_SCOPE) + set(KALSHI_WEBSOCKETS_VIA pkg-config PARENT_SCOPE) + elseif(required) + message(FATAL_ERROR "libwebsockets not found: install it with its CMake package or a " + "pkg-config file") endif() -else() - find_package(PkgConfig REQUIRED) - pkg_check_modules(WEBSOCKETS REQUIRED IMPORTED_TARGET libwebsockets) - set(KALSHI_WEBSOCKETS_TARGET PkgConfig::WEBSOCKETS) -endif() +endfunction() diff --git a/cmake/kalshiConfig.cmake.in b/cmake/kalshiConfig.cmake.in index 15bb27b..5ab7dba 100644 --- a/cmake/kalshiConfig.cmake.in +++ b/cmake/kalshiConfig.cmake.in @@ -4,7 +4,14 @@ include(CMakeFindDependencyMacro) find_dependency(OpenSSL 3.0) find_dependency(CURL) + include("${CMAKE_CURRENT_LIST_DIR}/KalshiWebsockets.cmake") +kalshi_find_websockets(FALSE) +if(NOT DEFINED KALSHI_WEBSOCKETS_TARGET) + set(kalshi_FOUND FALSE) + set(kalshi_NOT_FOUND_MESSAGE "kalshi-cpp needs libwebsockets, which was not found") + return() +endif() # The exported targets link the libwebsockets target this package was built # against; map it to whichever one was found here. if(NOT TARGET @KALSHI_WEBSOCKETS_TARGET@) diff --git a/tools/codegen/common.py b/tools/codegen/common.py index 405ffe8..c4bfea1 100644 --- a/tools/codegen/common.py +++ b/tools/codegen/common.py @@ -63,10 +63,12 @@ def first_sentence(text: str | None) -> str: sentence = match.group(1) if match else text if len(sentence) <= 300: return sentence - cut = sentence[:297].rsplit(" ", 1)[0] + cut = sentence[:297] + if sentence[297] != " ": # mid-word: back up to the last whole word + cut = cut.rsplit(" ", 1)[0] if cut.count("`") % 2: # never end inside a code span - cut = cut[:cut.rindex("`")].rstrip() - return cut + "..." + cut = cut[:cut.rindex("`")].rstrip() or cut.replace("`", "") + return cut.rstrip() + "..." def doc_lines(text: str, indent: str, width: int = 96) -> list[str]: