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
16 changes: 16 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Opens the project inside the compose `speller` container, so Ruby LSP (and
// its test explorer) run with the container's Ruby, gems and Aspell.
{
"name": "speller",
"dockerComposeFile": "../compose.yaml",
"service": "speller",
"workspaceFolder": "/app",
// Keep the dev server running when VS Code closes; it is managed with
// `podman compose` as well.
"shutdownAction": "none",
"customizations": {
"vscode": {
"extensions": ["Shopify.ruby-lsp"]
}
}
}
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.git
.bundle
.ruby-lsp
coverage
54 changes: 54 additions & 0 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: testing

on:
pull_request:
push:
branches:
- master

permissions:
contents: read

jobs:
tests:
runs-on: ubuntu-latest
env:
# rerun, ruby-lsp and debug are only for the dev container
BUNDLE_WITHOUT: development
steps:
- name: Checkout code
uses: actions/checkout@v7

# ffi-aspell loads libaspell at runtime and the suite checks against the
# real es/en/pt_BR dictionaries, the same ones the Dockerfile installs.
- name: Install Aspell and dictionaries
run: |
sudo apt-get update -qq
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
libaspell-dev aspell-en aspell-es aspell-pt-br

# The Ruby version comes from .ruby-version
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
bundler-cache: true

- name: Run tests with coverage
env:
COVERAGE: '1'
run: bundle exec rake test

- name: Coverage summary
run: |
echo "### Coverage" >> "$GITHUB_STEP_SUMMARY"
echo "Line coverage: $(jq -r '.result.line' coverage/.last_run.json)%" >> "$GITHUB_STEP_SUMMARY"

# TODO SonarQube: once the project exists, fix the paths in
# coverage/.resultset.json (sed $GITHUB_WORKSPACE -> /github/workspace/)
# and run LemontechSA/action-sonarqube-scan, as in green-mario.
- name: Upload coverage report
uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage/
include-hidden-files: true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage/
.DS_Store
2 changes: 1 addition & 1 deletion .ruby-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.2.2
4.0.9
5 changes: 5 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
// Ruby LSP runs inside the dev container (.devcontainer/), not on the host
"recommendations": ["ms-vscode-remote.remote-containers"],
"unwantedRecommendations": ["Shopify.ruby-lsp"]
}
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## Release 26.0.0
Wednesday, November 4th
* **Breaking** Actualiza a Ruby 4.0.9 y Sinatra 4.2.1 (Rack 3 + Puma). El servidor requiere Ruby 4.0
* **Breaking** Las posiciones (`o`) apuntan a la palabra limpia, sin la puntuación inicial, y ya no se desfasan con espacios múltiples o saltos de línea: `texto[o, l]` es la palabra marcada
* **Breaking** Los nombres de headers de respuesta salen en minúscula (Rack 3)
* **Fix** Las sugerencias con tilde ya no salen corruptas y las palabras con tilde bien escritas ya no se marcan como error. El encoding de aspell ya no depende del locale
* **Fix** La puntuación repetida (`Bueno...`, `¡¡Hola!!`) ya no se marca como error
* **Update** Reemplaza `sinatra-cross_origin` por headers CORS propios con los mismos valores
* **New** Entorno de desarrollo con Podman compose en el puerto 3019 y Dev Container para Ruby LSP en VS Code
* **New** Suite de tests con Minitest
* **New** GitHub Action que corre los tests con cobertura (SimpleCov) en cada PR y en `master`

## Hotfix 17.0.4
Monday, May 15th
* **Fix** [Agrega método options](https://github.com/5rabbits/speller/pull/3)
Expand Down
58 changes: 58 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# CLAUDE.md

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

## Overview

Single-file Sinatra service (`speller.rb`) that exposes Aspell spellchecking over HTTP. Ruby 4.0 (`.ruby-version`, enforced by the Gemfile and duplicated in the `Dockerfile` `FROM` line; keep both in sync), Sinatra 4.2 on Rack 3, Puma, `ffi-aspell` 1.1.

## Commands

Preferred: the Podman compose dev environment (`compose.yaml` + `Dockerfile`), which bundles Ruby, Aspell and the es/en/pt_BR dictionaries:

```
podman compose up --build -d # serves on localhost:3019
podman compose logs -f
curl -X POST localhost:3019/spell -d 'lang=en' --data-urlencode 'text=Helo wrld'
podman compose run --rm speller bundle install # after editing Gemfile (writes Gemfile.lock), then up --build
podman compose run --rm speller irb -r ./speller # console with Speller loaded
```

The repo is bind-mounted at `/app` and `rerun` restarts `rackup` (Puma) when a source file changes, within a few seconds. It must use `--force-polling` because host file events don't reach the container through the podman machine mount. Gemfile changes need a rebuild.

VS Code: `.devcontainer/devcontainer.json` attaches to the same compose container (`name: speller` in `compose.yaml` keeps Dev Containers from starting a second one on port 3019), and Ruby LSP runs there. `ruby-lsp` and `debug` are in the Gemfile's development group so the image already has them and Ruby LSP skips its composed bundle. Nothing Ruby-related is installed on the host. Dev Containers needs `"dev.containers.dockerPath": "podman"` in the user's VS Code settings.

Without containers, Aspell plus dictionaries must be installed on the host (`ffi-aspell` loads the native library at runtime). See README. Then `bundle install && ruby speller.rb` (port 4567).

Tests use Minitest + rack-test (`test/`) and need real Aspell dictionaries, so run them in the container:

```
podman compose run --rm speller bundle exec rake test # whole suite
podman compose run --rm speller bundle exec rake test TEST=test/speller_app_test.rb # one file
podman compose run --rm speller bundle exec ruby -Itest test/speller_test.rb -n test_check_returns_nil_without_text # one test
podman compose run --rm -e COVERAGE=1 speller bundle exec rake test # with SimpleCov report in coverage/
```

Coverage is opt-in (`COVERAGE=1`, as in timeflow). SimpleCov 1.x starts at the top of `test/test_helper.rb`, before `speller.rb` is required, and only covers `speller.rb` (`cover`; 1.x deprecates `track_files`/`add_filter`). `coverage/` is gitignored.

CI: `.github/workflows/testing.yml` runs on every PR and on push to `master`. It installs Aspell and the dictionaries with apt, uses `ruby/setup-ruby` (version from `.ruby-version`), runs the suite with `COVERAGE=1`, writes line coverage to the job summary and uploads `coverage/` as an artifact (with `.resultset.json`). SonarQube isn't set up yet because the project doesn't exist; the workflow has a TODO where the scan goes.

There is no linter config.

## Architecture

`speller.rb` holds two classes. `SpellerApp.run!` only fires when the file is executed directly (port 4567, or `PORT`); `config.ru` mounts `SpellerApp` for `rackup`, and requiring `speller.rb` does not boot a server.

- `SpellerApp` (modular `Sinatra::Base`): `POST /spell` with `text` and `lang` params (form body or query string), plus `OPTIONS /spell` for CORS preflight. There is no GET route. CORS is hand-rolled in a `before` filter: when a request carries `Origin`, it is echoed back along with `CORS_HEADERS`. These values match what the old `sinatra-cross_origin` gem sent, and `test_options_answers_cors_preflight` pins them.
- **Host authorization (Sinatra 4.1+):** by default, Sinatra 403s requests in `development` whose `Host` isn't `localhost`, `*.localhost`, `*.test` or an IP. `SpellerApp` turns that off (`permitted_hosts: []` in `configure :development`), so any host is accepted in every environment. This is a deliberate, temporary choice to revisit later. The tests run as `test`, so they don't cover this setting.
- `Speller.check(text, lang)`: builds a fresh `FFI::Aspell::Speller` per request. `Speller.words` scans whitespace-separated tokens, strips all leading and trailing punctuation (punctuation-only tokens are skipped), and keeps each cleaned word's offset. `check` returns only misspelled words. Returns `nil` (JSON `null`) when `text` is missing.

Behavior that is easy to break:

- **Languages:** only `es`, `en`, `pt` are accepted; anything else silently falls back to `es`. `pt` maps to the `pt_BR` Aspell dictionary.
- **Response shape:** `{ attrs: { l:, o:, s: 1 }, suggestions: [...] }`, at most 10 suggestions. `o` is the character offset (not byte) of the cleaned word in the original text and `l` is its length, so `text[o, l]` is exactly the flagged word, whatever whitespace or punctuation surrounds it. `s` is always `1`. The `flagged` helper in `test/speller_test.rb` asserts this. Clients rely on this shape.
- **Encoding:** `Speller.check` creates the speller with `'encoding' => 'utf-8'`, so input and suggestions are UTF-8 whatever the locale or `ASPELL_CONF` say. The option must stay ahead of the language: ffi-aspell applies options in hash order, and with no locale the default encoding is `none`, which raises `unknown encoding name - none`. Don't bring back the old `force_encoding('ISO-8859-1')` workaround (hotfixes 17.0.2/17.0.3). It garbled accented suggestions (`corazón`) or flagged correct accented words, depending on the environment. The accented-text tests in `test/speller_test.rb` cover both cases.

## Releases

Versions use `YY.minor.patch` (e.g. `17.0.4`) in `VERSION`. Hotfixes go on `hotfix/<name>` branches, bump `VERSION`, and add an entry to `CHANGELOG.md` (date plus a `**Fix**` line linking the PR). Changelog entries and older commits are often in Spanish.
27 changes: 27 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Development image for the speller service.
# Keep the Ruby version in sync with .ruby-version (the Gemfile enforces it).
FROM docker.io/library/ruby:4.0.9-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
build-essential \
git \
libaspell-dev \
aspell-en \
aspell-es \
aspell-pt-br \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY Gemfile Gemfile.lock .ruby-version ./
RUN bundle install

COPY . .

EXPOSE 3019

# rerun restarts the server on source changes. Polling is required: file events
# from the host do not reach the container through the podman machine mount.
CMD ["bundle", "exec", "rerun", "--background", "--force-polling", "--", \
"rackup", "--host", "0.0.0.0", "--port", "3019"]
32 changes: 27 additions & 5 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
source 'https://rubygems.org'

ruby file: '.ruby-version'

# Classy web-development dressed in a DSL
gem 'sinatra'
gem 'sinatra', '~> 4.2'

# Rack 3 moved the server launcher (rackup, SpellerApp.run!) out of rack
gem 'rackup', '~> 2.3'
gem 'puma', '~> 8.0'

# Load aspell dynamically for spellcheck
gem 'ffi-aspell', '~> 1.1'

# Load aspell dinamically for spellcheck
gem 'ffi-aspell', '1.0.2'
group :development do
# Restarts the server when source files change
gem 'rerun', '~> 0.14'
# Language server and debugger for the VS Code Ruby LSP extension (run in the
# container). Kept here so the image has them and Ruby LSP installs nothing.
gem 'ruby-lsp', '~> 0.26', require: false
gem 'debug', '~> 1.11', require: false
end

# Cross Origin Request Sharing extension for Sinatra
gem 'sinatra-cross_origin', '0.3.2'
group :test do
gem 'minitest', '~> 6.0'
gem 'rake', '~> 13.4'
# Drives the Sinatra app in request tests
gem 'rack-test', '~> 2.3'
# Coverage report, opt-in with COVERAGE=1 (see test/test_helper.rb)
gem 'simplecov', '~> 1.3', require: false
end
Loading
Loading