Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fake-cli": {
"version": "6.1.4",
"commands": [
"fake"
]
},
"paket": {
"version": "10.3.1",
"commands": [
"paket"
]
},
"dotnet-fsharplint": {
"version": "0.26.10",
"commands": [
"dotnet-fsharplint"
]
}
}
}
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Paket dependency manager
.paket/
packages/
paket-files/

# Ionide
.ionide/

# FAKE - F# Make
.fake/

# Released
release/*.nupkg
131 changes: 131 additions & 0 deletions .github/skills/proto/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
name: proto
description: "Use when authoring or editing Protocol Buffer (.proto) files for gRPC service contracts that use the Feather core types and conventions."
---

# Protocol Buffers / gRPC Skill

Conventions for writing `.proto` gRPC service contracts that interoperate with
`Feather.Grpc` and the shared core types from
[grpc.contract.core](https://github.com/FeatherTools/grpc.contract.core)
(`Spot`, `Error`, `Timestamp`, `CorrelationId`, `Instance`, `Box`,
`SerializedForChunking`, …).

## File Structure

```
proto/
<systemName>/ # one folder per system (camelCase)
<service_or_feature>.proto # one file per logical service/feature
```

Group `.proto` files by system, one file per logical service or feature. Reference the
shared core types from `grpc.contract.core` rather than redefining them.

---

## File Header Template

```protobuf
syntax = "proto3";

package <system_name>; // snake_case, matches folder name

import "feather/core.proto";
// add other imports as needed

option csharp_namespace = "<Namespace>"; // PascalCase
```

Add `option php_namespace`, `go_package`, etc. for whichever languages you generate.

---

## Naming Conventions

- Message names: `PascalCase`
- Request messages: `<MethodName>Request`
- Response messages: `<MethodName>Response`
- Field names: `snake_case`
- Enum values: `UPPER_SNAKE_CASE`
- Service names: `<ServiceName>Service` (or `<ServiceName>`)

---

## Standard Response Pattern

**All responses use `oneof result { Success / Error }`:**

```protobuf
message <Method>Response {
oneof result {
Success success = 1;
feather.Error error = 2;
}

message Success {
<ReturnType> <field_name> = 1;
}
}
```

Never return bare fields at the top level of a response — always wrap them in the
`oneof result` pattern. On the F# side this maps cleanly onto
`HighLevel.Response.handle`, which produces either a `Success` or an `Error` response.

---

## Service Definition

```protobuf
service <ServiceName>Service {
rpc <MethodName> (<MethodName>Request) returns (<MethodName>Response);
}
```

---

## Streaming Large Payloads

For large or compressible payloads, stream `feather.SerializedForChunking` chunks
instead of a single message. The sender serializes and chunks the payload; the receiver
reassembles it. On the F# side this is handled by `SerializedForChunking` (plain, gzip,
raw bytes or text) — see the `Feather.Grpc` README.

```protobuf
message SendDocumentRequest {
feather.SerializedForChunking chunk = 1;
}

service DocumentService {
rpc SendDocument (stream SendDocumentRequest) returns (SendDocumentResponse);
}
```

---

## Security Rules in Proto

- Do not put personal or sensitive data as plain fields — carry it as opaque `bytes`
(e.g. an encrypted envelope) and decrypt at the domain boundary.
- Auth/identity info (tokens, JWTs) is extracted by server interceptors — do NOT include
them in request messages. Add a comment `// Extracted from JWT by auth interceptor`
where relevant.

---

## What does NOT belong in proto files

Proto files represent gRPC service contracts only — request/response messages and
service definitions. Domain-internal concerns (events, stream carriers, background
messages such as `*Event` / `*Stream`) must never be added to `.proto` files, even when
they reference types that do have proto equivalents.

---

## Workflow: Adding a new RPC method

1. Add `<Method>Request` and `<Method>Response` messages (Response uses the `oneof result` pattern).
2. Add the `rpc` entry to the `service` block.
3. Regenerate stubs for your target languages.
4. Lint the proto (e.g. `protolint`) to verify formatting.
24 changes: 24 additions & 0 deletions .github/workflows/pr-check.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Pull request check

on: [pull_request]

jobs:
block-fixup-merge:
runs-on: ubuntu-latest
name: Block fixup commits

steps:
- uses: actions/checkout@v6

- name: Block fixup commit merge
uses: 13rac1/block-fixup-merge-action@v2.0.0

shellcheck: # https://github.com/marketplace/actions/shellcheck
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:
SHELLCHECK_OPTS: -e SC1090
27 changes: 27 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Publish

on:
push:
tags:
- '[0-9]+\.[0-9]+\.[0-9]+'

jobs:
publish:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- uses: actions/checkout@v6

- name: Setup .NET Core
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.x

- name: Publish to NuGet.org
env:
NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
DOTNET_ROLL_FORWARD: latestMajor
run: ./build.sh -t publish no-lint
31 changes: 31 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Tests

on:
#push:
pull_request:
schedule:
- cron: '0 3 * * *'

jobs:
tests:
strategy:
matrix:
os:
- name: ubuntu-latest
run: ./build.sh
runs-on: ${{ matrix.os.name }}

steps:
- uses: actions/checkout@v6

- name: Setup .NET Core
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.x

- name: Run tests
env:
PRIVATE_FEED_USER: ${{ github.repository_owner }}
PRIVATE_FEED_PASS: ${{ secrets.GITHUB_TOKEN }}
DOTNET_ROLL_FORWARD: latestMajor
run: ${{ matrix.os.run }} -t tests no-lint # temporary no-lint to unblock CI
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
AssemblyInfo.fs

# Paket dependency manager
.paket/
packages/
paket-files/

# Ionide
.ionide/

# FAKE - F# Make
.fake/

# Released
release/*.nupkg
71 changes: 71 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# AGENTS.md

Guidance for AI coding agents working in this repository.

## What this is

`Feather.Grpc` is an F# library published to NuGet (`Feather.Grpc`) providing low- and
high-level helpers for building gRPC clients and servers. It targets **.NET 10** and
sits on top of `Grpc.Net.Client` / `Grpc.Core.Api`.

The code was extracted from a larger internal service and is now a standalone public
package. It is opinionated toward the `Feather.*` / `Alma.*` ecosystem (contracts,
error handling, cryptography, service identification).

## Layout

Everything compiles from `Grpc.fsproj`; compile order matters in F#, so keep the
`<Compile>` order in the `.fsproj` consistent with dependencies between files.

- `src/Utils.fs` — internal helpers (`Guid`, `DateTimeOffset`, `Gzip`, `Async`).
- `src/Grpc.fs` — low-level channel creation and `AsyncSeq` ⟷ gRPC stream conversions.
- `src/Error.fs` — `ContractError`, `GrpcError`, and computation-expression extensions.
- `src/CoreTypes.fs` — domain types and contract conversions (`Timestamp`, `CorrelationId`, `Spot`, `Instance`, `Box`).
- `src/Auth.fs` — `AuthInterceptor` / `AsyncAuthInterceptor` for server-side auth.
- `src/Metrics.fs` — `GrpcMetrics` error counters.
- `src/Serialization.fs` — `SerializedForChunking`: serialize + chunk large payloads for streaming (plain, gzip, raw parts, text).
- `src/HighLevel.fs` — high-level `Read` / `Send` / `Duplex` / `Response` orchestration built on the modules above.
- `tests/` — Expecto test suite mirroring the module structure.

## Build & test

Use the build script (restores tools + Paket, then runs the FAKE targets):

```bash
./build.sh build # build the library
./build.sh -t tests # run the Expecto test suite
```

Dependencies are managed with **Paket**, not raw `PackageReference`. To change
dependencies edit `paket.dependencies` + `paket.references`, then let the build
script restore. Do not hand-edit lock files.

## Conventions

- **No abbreviations** in code or domain names. Use `Language`, not `Lang`; `Latitude`,
not `Lat`. The only exception is the proper name of an adopted standard, which is
written FULLY UPPERCASE (e.g. `JWT`, `GERSId`).
- Prefer established open standards for data representation (ISO 8601 for time, etc.)
so data migrates between tools without translation.
- Follow the existing functional style: `Result` / `AsyncResult` for errors,
`[<RequireQualifiedAccess>]` modules, `AsyncSeq` for streaming. Avoid exceptions for
control flow — convert to `GrpcError` / `ContractError` at boundaries.
- Keep the contract boundary explicit: `ofContract` / `asContract` pairs convert between
`Feather.Contracts.*` proto types and domain types.
- Lint config lives in `fsharplint.json`.

## Release

Releasing is manual (see `README.md`):

1. Bump `<Version>` in `Grpc.fsproj`.
2. Add an entry to `CHANGELOG.md` (there is always an `Unreleased` section at the top;
use `Add` / `Changed` / `Fix` / `Removed` subsections). Mark breaking changes with `[**BC**]`.
3. Commit the new version and tag it.

## When editing

- Read the target file and its neighbours first; F# is order-sensitive.
- Add or update tests in `tests/` alongside behavioural changes.
- Only make the change requested — no unsolicited refactors or extra files.
- Do not add a `.gitignore` or other scaffolding unless explicitly asked.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

<!-- There is always Unreleased section on the top. Subsections (Add, Changed, Fix, Removed) should be Add as needed. -->
## Unreleased

## 0.0.0 -
- Initial implementation
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
See [AGENTS.md](./AGENTS.md) for repository guidance, build/test commands, layout, and conventions.
Loading