atproto libraries implementation in ocaml

Contributing to AT Protocol OCaml#

Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to the AT Protocol OCaml libraries.

Getting Started#

Prerequisites#

  • OCaml >= 5.1 (5.4 recommended)
  • opam >= 2.0
  • dune >= 3.20

Setting Up Development Environment#

# Clone the repository
git clone https://github.com/gdiazlo/atproto.git
cd atproto

# Install dependencies
opam install . --deps-only --with-test

# Build the project
dune build

# Run tests
dune runtest

Project Structure#

atproto/
├── lib/                    # Library source code
│   ├── api/               # High-level API client
│   ├── crypto/            # Cryptographic operations
│   ├── effects/           # I/O effect abstractions
│   ├── identity/          # DID/Handle resolution
│   ├── ipld/              # DAG-CBOR, CIDs, CAR files
│   ├── lexicon/           # Schema parsing/validation
│   ├── mst/               # Merkle Search Tree
│   ├── multibase/         # Base encoding
│   ├── repo/              # Repository operations
│   ├── sync/              # Firehose and sync
│   ├── syntax/            # Identifier parsing
│   └── xrpc/              # XRPC client/server
├── test/                   # Test suites
│   ├── fixtures/          # Test data (atproto-interop-tests)
│   └── */                 # Per-package tests
├── examples/              # Example applications
└── dune-project           # Project configuration

Development Workflow#

Running Tests#

# Run all tests
dune runtest

# Run tests for a specific package
dune runtest test/syntax

# Run with verbose output
dune runtest --force --verbose

# Run a specific test file
dune exec test/syntax/test_syntax.exe

Code Formatting#

We use ocamlformat for consistent code style:

# Format all files
dune fmt

# Check formatting without modifying
dune fmt --preview

The project uses the default ocamlformat configuration (see .ocamlformat).

Building Documentation#

# Build API documentation
dune build @doc

# Open in browser
open _build/default/_doc/_html/index.html

Coding Guidelines#

General Principles#

  1. Functional first - Prefer immutable data structures and pure functions
  2. Type safety - Use the type system to prevent errors at compile time
  3. No regex - All syntax validation uses hand-written parsers
  4. Effects for I/O - Use the effects system for all I/O operations

Module Structure#

Each package follows a consistent structure:

lib/packagename/
├── dune                      # Build configuration
├── atproto_packagename.ml    # Public interface (re-exports)
├── module1.ml                # Implementation
├── module1.mli               # Interface (optional but recommended)
└── ...

Code Style#

(* Use descriptive names *)
let validate_handle s = ...

(* Document public functions *)
(** [of_string s] parses [s] as a handle.
    Returns [Error msg] if the string is not a valid handle. *)
val of_string : string -> (t, string) result

(* Prefer Result over exceptions for expected errors *)
let parse input =
  match ... with
  | Some v -> Ok v
  | None -> Error "invalid input"

(* Use labeled arguments for clarity when appropriate *)
let create ~did ~handle ~service_endpoint = ...

Error Handling#

  • Use Result for recoverable errors
  • Use Option for optional values
  • Reserve exceptions for programming errors (bugs)
  • Provide descriptive error messages

Testing#

  • Every module should have corresponding tests
  • Use the interop test fixtures where applicable
  • Test edge cases and error conditions
  • Structure tests with clear descriptions
let test_handle_valid () =
  let cases = ["alice.bsky.social"; "test.example.com"] in
  List.iter (fun s ->
    match Handle.of_string s with
    | Ok _ -> ()
    | Error e -> Alcotest.fail (Printf.sprintf "%s: %s" s e)
  ) cases

let () =
  Alcotest.run "Handle" [
    "parsing", [
      Alcotest.test_case "valid handles" `Quick test_handle_valid;
      Alcotest.test_case "invalid handles" `Quick test_handle_invalid;
    ];
  ]

Making Changes#

Before You Start#

  1. Check existing issues to avoid duplicate work
  2. For large changes, open an issue first to discuss the approach
  3. Make sure you understand the AT Protocol specs at atproto.com

Pull Request Process#

  1. Fork and branch - Create a feature branch from main

    git checkout -b feature/my-feature
    
  2. Make changes - Follow the coding guidelines above

  3. Test - Ensure all tests pass

    dune runtest
    
  4. Format - Run the formatter

    dune fmt
    
  5. Commit - Write clear commit messages

    Add handle resolution via DNS TXT records
    
    Implements DNS-based handle resolution as specified in
    the AT Protocol identity spec. Includes tests using
    fixtures from atproto-interop-tests.
    
  6. Push and PR - Open a pull request with:

    • Clear description of changes
    • Reference to related issues
    • Test coverage for new functionality

Commit Message Format#

<type>: <short summary>

<detailed description if needed>

<references>

Types:

  • feat - New feature
  • fix - Bug fix
  • docs - Documentation only
  • test - Adding or updating tests
  • refactor - Code change that doesn't fix a bug or add a feature
  • chore - Maintenance tasks

Working with the AT Protocol Spec#

Key Resources#

Interop Tests#

The test fixtures in test/fixtures/ come from the official atproto-interop-tests repository.

To update fixtures:

cd test/fixtures
git pull origin main

When implementing new functionality, check if there are relevant fixtures to test against.

Package-Specific Notes#

atproto-syntax#

  • No regex - all parsing is done with hand-written parsers
  • Each identifier type has of_string, to_string, and validation
  • Follow the exact spec for character sets and lengths

atproto-crypto#

  • Uses mirage-crypto-ec for P-256
  • Uses secp256k1-ml indirectly via custom K-256 wrapper
  • Always apply low-S normalization to signatures
  • did:key encoding must match multicodec spec exactly

atproto-ipld#

  • DAG-CBOR keys must be sorted by length, then lexicographically
  • CIDs use SHA-256 and base32lower encoding
  • Support both CIDv0 and CIDv1

atproto-effects#

  • Define effect types for all I/O operations
  • Keep effect handlers separate from business logic
  • Test with mock handlers, deploy with real handlers

Questions?#

  • Open an issue for questions about the codebase
  • Check the AT Protocol Discord for protocol questions
  • Review the reference TypeScript implementation for clarification

License#

By contributing, you agree that your contributions will be licensed under the MIT License.