diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d05a424c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# Git +.git +.gitignore +.github + +# Claude/AI configuration +.claude +.serena +CLAUDE.md + +# Documentation +*.md +!README.md + +# Test artifacts +coverage/ +test_db +*.sqlite3 +.last_run.json +.resultset.json + +# Ruby/bundler +.bundle +vendor/bundle + +# OS files +.DS_Store +Thumbs.db + +# Editor files +.vscode +.idea +*.swp +*.swo +*~ diff --git a/.gitignore b/.gitignore index 800c71c6..2f7f9cb6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ Gemfile.lock InstalledFiles _yardoc -coverage +coverage/ doc/ lib/bundler/man pkg @@ -17,9 +17,10 @@ spec/reports test/tmp test/version_tmp tmp -coverage test/log +log/*.log test_db test_db-journal .idea *.iml +.claude/settings.local.json diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000..14d86ad6 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..abee1f86 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,91 @@ +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp csharp_omnisharp +# dart elixir elm erlang fortran go +# haskell java julia kotlin lua markdown +# nix perl php python python_jedi r +# rego ruby ruby_solargraph rust scala swift +# terraform typescript typescript_vts zig +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# Special requirements: +# - csharp: Requires the presence of a .sln file in the project folder. +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- ruby + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# whether to use the project's gitignore file to ignore files +# Added on 2025-04-07 +ignore_all_files_in_gitignore: true + +# list of additional paths to ignore +# same syntax as gitignore, so you can use * and ** +# Was previously called `ignored_dirs`, please update your config if you are using that. +# Added (renamed) on 2025-04-07 +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project by name. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_lines`: Deletes a range of lines within a file. +# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. +# * `execute_shell_command`: Executes a shell command. +# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. +# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). +# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Gets the initial instructions for the current project. +# Should only be used in settings where the system prompt cannot be set, +# e.g. in clients you have no control over, like Claude Desktop. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_at_line`: Inserts content at a given line in a file. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: Lists memories in Serena's project-specific memory store. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. +# * `remove_project`: Removes a project from the Serena configuration. +# * `replace_lines`: Replaces a range of lines within a file with new content. +# * `replace_symbol_body`: Replaces the full definition of a symbol. +# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. +# * `switch_modes`: Activates modes by providing a list of their names +# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. +# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. +# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. +# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. +excluded_tools: [] + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: | + This is the jsonapi-resources Ruby gem project. Key information: + - Framework for JSON:API compliant Rails applications + - Supports Rails 5.1+ (targeting Rails 7-8 support) + - Uses Docker for multi-version testing (see docker-compose.yml) + - Main codebase in lib/jsonapi/ + - Tests in test/ directory + - See CLAUDE.md for detailed architecture and development guidance + +project_name: "jsonapi-resources" +included_optional_tools: [] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2a217d03 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,376 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +`jsonapi-resources` (JR) is a Ruby gem that provides a framework for developing JSON:API compliant API servers in Rails. It's resource-centric, requiring mainly resource definitions (attributes, relationships) to achieve full JSON:API compliance. + +**Supported Rails Versions:** +- Rails 6.1.7.10 ✅ +- Rails 7.0.10 ✅ +- Rails 7.1+ (in progress) +- Rails 8.0+ (in progress) + +## Development Commands + +### Testing +```bash +# Run full test suite +rake test + +# Run single test file +ruby -I test test/unit/resource/resource_test.rb + +# Run single test case +ruby -I test test/unit/resource/resource_test.rb -n test_method_name + +# Run with coverage +COVERAGE=true bundle exec rake test + +# Test specific Rails version +export RAILS_VERSION=6.1.1; bundle update; bundle exec rake test + +# Run with specific seed for reproducibility +export RAILS_VERSION=6.1.1; bundle update; bundle exec rake TESTOPTS="--seed=39333" test + +# Run benchmarks +rake test:benchmark + +# Run with coverage measurement +COVERAGE=true bundle exec rake test + +# View coverage report (after running with COVERAGE=true) +open coverage/index.html # macOS +xdg-open coverage/index.html # Linux +``` + +Coverage reports are generated using SimpleCov and saved to the `coverage/` directory. The configuration includes: +- Grouped coverage by component (Controllers, Resources, Serializers, etc.) +- Branch coverage tracking (Ruby 2.5+) +- Both HTML and console output formats +- Automatic exclusion of test files + +### Docker Testing + +The project includes Docker support for testing across multiple Rails versions: + +```bash +# Test with specific Rails version +docker-compose run rails-6.1 # Rails 6.1.7.10 +docker-compose run rails-7.0 # Rails 7.0.10 +docker-compose run rails-7.1 # Rails 7.1.6 +docker-compose run rails-7.2 # Rails 7.2.3 +docker-compose run rails-8.0 # Rails 8.0.4 +docker-compose run rails-8.1 # Rails 8.1.1 + +# Interactive shell with specific Rails version +docker-compose run shell +RAILS_VERSION=7.0.10 docker-compose run shell + +# Run tests in interactive shell +docker-compose run shell +# Inside container: +bundle update +bundle exec rake test + +# Run with coverage in Docker +docker-compose run -e COVERAGE=true rails-6.1 +# Or in interactive shell: +docker-compose run shell +# Inside container: +COVERAGE=true bundle exec rake test + +# Build/rebuild images +docker-compose build + +# Clean up +docker-compose down -v # Remove containers and volumes +``` + +Docker configuration files: +- `Dockerfile` - Container image definition (Ruby 3.2 + dependencies) +- `docker-compose.yml` - Service definitions for each Rails version +- `.dockerignore` - Files excluded from Docker context + +### Database Setup +Tests use SQLite by default. The database is configured via `ENV['DATABASE_URL']` (defaults to `sqlite3:test_db`). DatabaseCleaner handles cleanup between tests. + +## Architecture + +### Core Request Flow + +The library follows a layered architecture: + +``` +HTTP Request → ResourceController → Request → Operation → Processor → Resource → ActiveRecord Model + ↓ +HTTP Response ← ResponseDocument ← Serializer ← OperationResult ← ResourceSet +``` + +**Key layers:** + +1. **ResourceController** (`ActsAsResourceController` mixin): Handles HTTP lifecycle, verifies headers (`application/vnd.api+json`), delegates to Request +2. **Request**: Parses filters, sorts, includes, pagination, sparse fieldsets; creates Operation objects +3. **Operation**: Discrete units of work (find, show, create_resource, etc.); delegates to Processor +4. **Processor**: Executes operations, calls Resource methods, returns OperationResults +5. **Resource**: Defines API resources with attributes, relationships, filters, sorts; bridges to ActiveRecord +6. **ResourceSet**: Collection of ResourceFragments populated from database (with optional caching) +7. **Serializer**: Converts ResourceSets to JSON:API format (primary data + included) + +### Resource Hierarchy + +Three-tier inheritance provides flexibility: + +- **BasicResource**: Pure Ruby, no ActiveRecord dependency +- **ActiveRelationResource**: Adds ActiveRecord query building via `records()`, filtering, sorting, pagination +- **Resource**: Default entry point (includes `root_resource` designation) + +Resources define the API surface: + +```ruby +class ArticleResource < JSONAPI::Resource + attributes :title, :body + has_one :author + has_many :comments + + filter :title, apply: ->(records, value, _options) { + records.where(title: value) + } + sort :created_at +end +``` + +### JoinManager + +The `JoinManager` (in `lib/jsonapi/active_relation/join_manager.rb`) is critical for performance. It: + +- Consolidates JOIN requirements from relationships, filters, and sorts +- Tracks table aliases to prevent collisions +- Builds optimized queries with minimal joins +- Supports LEFT JOINs via adapters + +When working with complex filters or sorts that require joins, understand JoinManager's role in query construction. + +### Fragment-Based Architecture + +Rather than eagerly loading full ActiveRecord models, JR uses: + +- **ResourceFragment**: Lightweight data holder (identity + cache_field + partial attributes) +- **ResourceTree**: Hierarchical structure during query phase (mirrors included relationships) +- **ResourceSet**: Flattened collection for serialization + +This enables efficient caching and avoids N+1 queries. The `ResourceSet.populate!` method checks cache, fetches missing resources, and updates cache. + +### Caching Strategy + +Fragment caching operates at the resource level: + +- Cache key: resource type + id + cache_field hash + serializer config + context +- Supports any `ActiveSupport::Cache` store +- Configured via `CachedResponseFragment` +- Multi-read/write for efficiency + +Cache is invalidated when the `cache_field` (typically `updated_at`) changes. + +### Configuration + +Global configuration in `JSONAPI.configure`: + +```ruby +JSONAPI.configure do |config| + config.json_key_format = :dasherized_key # or :camelized_key, :underscored_key + config.route_format = :dasherized_route + config.resource_key_type = :integer # or :uuid, :string + config.allow_sort = true + config.allow_filter = true + config.default_paginator = :paged # or :offset, :none + config.resource_cache = Rails.cache +end +``` + +Test configuration uses `:camelized_key` format (see `test/test_helper.rb:42`). + +### Routing + +Routes use `jsonapi_resources` helper: + +```ruby +jsonapi_resources :articles do + jsonapi_relationships # Adds relationship routes + jsonapi_links :special_tags # Custom link routes +end +``` + +Creates: +- Collection: `GET /articles`, `POST /articles` +- Member: `GET /articles/:id`, `PATCH /articles/:id`, `DELETE /articles/:id` +- Relationships: `GET/POST/PATCH/DELETE /articles/:id/relationships/:name` +- Related: `GET /articles/:id/:name` + +Route format configurable per namespace (`:underscored_route`, `:dasherized_route`, `:camelized_route`). + +## Important Patterns + +### Context Pattern + +A `context` hash flows through the entire request lifecycle. Use for authorization, scoping, tenant isolation: + +```ruby +class ArticlesController < JSONAPI::ResourceController + def context + {current_user: current_user, tenant_id: current_tenant.id} + end +end + +class ArticleResource < JSONAPI::Resource + def records(options = {}) + super.where(tenant_id: context[:tenant_id]) + end +end +``` + +### Callbacks + +Resource callbacks wrap lifecycle events: + +```ruby +class ArticleResource < JSONAPI::Resource + before_save :audit_change + after_create :send_notification + + # Callbacks: :create, :update, :remove, :save, :replace_fields + # Also: :replace_to_one_relationship, :replace_to_many_relationship, etc. +end +``` + +### Custom Processors + +Override default operation handling per resource: + +```ruby +class ArticleResource < JSONAPI::Resource + def self.processor_class + ArticleProcessor + end +end + +class ArticleProcessor < JSONAPI::Processor + def find + # Custom find logic + end +end +``` + +### Formatters + +Pluggable formatters control key/route transformations: + +- `JSONAPI::KeyFormatter`: `:dasherized_key`, `:camelized_key`, `:underscored_key` +- `JSONAPI::RouteFormatter`: `:dasherized_route`, `:camelized_route`, `:underscored_route` +- Custom formatters: Inherit from base classes (see `test/test_helper.rb:653-704`) + +### Relationship Reflection + +Optional feature to automatically update inverse relationships: + +```ruby +JSONAPI.configure do |config| + config.use_relationship_reflection = true +end +``` + +Maintains referential integrity when creating/updating relationships. + +## Testing Patterns + +### Test Structure + +Tests are organized by: + +- `test/controllers/` - Controller tests +- `test/unit/resource/` - Resource tests +- `test/unit/serializer/` - Serializer tests +- `test/unit/processor/` - Processor tests +- `test/integration/` - Integration tests + +### Test Helpers + +Located in `test/helpers/`: + +- `assertions.rb` - Custom assertions +- `functional_helpers.rb` - Controller test helpers +- `value_matchers.rb` - Value matching utilities +- `configuration_helpers.rb` - Config manipulation + +### Assertions + +Common assertions: + +```ruby +# Query count tracking +assert_query_count(3) do + # code that should execute exactly 3 queries +end + +# JSON:API response validation +assert_jsonapi_response 200 +assert_jsonapi_get url + +# Caching validation +assert_cacheable_jsonapi_get url +assert_cacheable_get :index, params: {filter: {title: 'foo'}} +``` + +### Testing with Different Rails Versions + +The gem supports Rails 5.1+. CI matrix includes multiple Rails versions. Test against specific version: + +```bash +export RAILS_VERSION=6.0.3.4; bundle update; bundle exec rake test +``` + +## Key Files for Common Tasks + +### Adding New Resource Features + +- `lib/jsonapi/basic_resource.rb` - Core resource logic, callbacks, field definitions +- `lib/jsonapi/active_relation_resource.rb` - ActiveRecord integration, filtering, sorting + +### Modifying Request Handling + +- `lib/jsonapi/acts_as_resource_controller.rb` - HTTP handling, header verification +- `lib/jsonapi/request.rb` - Parameter parsing, operation creation + +### Changing Serialization + +- `lib/jsonapi/resource_serializer.rb` - Primary serialization logic +- `lib/jsonapi/response_document.rb` - Response structure (meta, links, errors) +- `lib/jsonapi/cached_response_fragment.rb` - Caching layer + +### Query Optimization + +- `lib/jsonapi/active_relation/join_manager.rb` - JOIN consolidation +- `lib/jsonapi/resource_set.rb` - Fragment population, cache management + +### Routing + +- `lib/jsonapi/routing_ext.rb` - Rails routing extensions + +## Conventions + +- Resources are singular: `ArticleResource` +- Controllers are plural: `ArticlesController` +- Models are singular: `Article` +- Foreign keys: `author_id` (for `has_one :author`) +- Polymorphic relationships supported via `polymorphic: true` +- Resource discovery: `Api::V1::ArticlesController` → `Api::V1::ArticleResource` + +## Common Gotchas + +- Relationship routes excluded by default in relationship blocks unless `jsonapi_relationships` called +- `config.action_controller.action_on_unpermitted_parameters = :raise` recommended for debugging +- MIME type enforcement is strict: requests must have `Accept: application/vnd.api+json` and `Content-Type: application/vnd.api+json` +- Resource caching requires `cache_field` (default: `updated_at`) to exist on models +- JoinManager aliasing can cause confusion; check generated SQL when debugging complex queries +- Transaction support requires `allow_transactions` config enabled diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..231ddbd6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Dockerfile for testing jsonapi-resources with multiple Rails versions +FROM ruby:3.2 + +# Install dependencies +RUN apt-get update -qq && \ + apt-get install -y build-essential libsqlite3-dev nodejs && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy gemspec and Gemfile first for better caching +COPY jsonapi-resources.gemspec Gemfile ./ +COPY lib/jsonapi/resources/version.rb ./lib/jsonapi/resources/ + +# Install bundler +RUN gem install bundler + +# Set default Rails version (can be overridden via build arg or env var) +ARG RAILS_VERSION=6.1.7.10 +ENV RAILS_VERSION=${RAILS_VERSION} + +# Install dependencies +RUN bundle install + +# Copy the rest of the application +COPY . . + +# Default command runs tests +CMD ["bundle", "exec", "rake", "test"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..8a314d5a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +services: + # Base service definition + test-base: &test-base + build: + context: . + dockerfile: Dockerfile + volumes: + - .:/app + - bundle-cache:/usr/local/bundle + working_dir: /app + stdin_open: true + tty: true + + # Rails 6.1.7.10 + rails-6.1: + <<: *test-base + container_name: jsonapi-rails-6.1 + environment: + - RAILS_VERSION=6.1.7.10 + command: bash -c "bundle update && bundle exec rake test" + + # Rails 7.0.10 + rails-7.0: + <<: *test-base + container_name: jsonapi-rails-7.0 + environment: + - RAILS_VERSION=7.0.10 + command: bash -c "bundle update && bundle exec rake test" + + # Rails 7.1.6 + rails-7.1: + <<: *test-base + container_name: jsonapi-rails-7.1 + environment: + - RAILS_VERSION=7.1.6 + command: bash -c "bundle update && bundle exec rake test" + + # Rails 7.2.3 + rails-7.2: + <<: *test-base + container_name: jsonapi-rails-7.2 + environment: + - RAILS_VERSION=7.2.3 + command: bash -c "bundle update && bundle exec rake test" + + # Rails 8.0.4 + rails-8.0: + <<: *test-base + container_name: jsonapi-rails-8.0 + environment: + - RAILS_VERSION=8.0.4 + command: bash -c "bundle update && bundle exec rake test" + + # Rails 8.1.1 + rails-8.1: + <<: *test-base + container_name: jsonapi-rails-8.1 + environment: + - RAILS_VERSION=8.1.1 + command: bash -c "bundle update && bundle exec rake test" + + # Interactive shell for debugging (defaults to Rails 6.1) + shell: + <<: *test-base + container_name: jsonapi-shell + environment: + - RAILS_VERSION=${RAILS_VERSION:-6.1.7.10} + command: /bin/bash + +volumes: + bundle-cache: