Skip to content

Commit bb5476c

Browse files
ci: add typo, packaging, and API coverage guardrails (#46)
1 parent 814bd0c commit bb5476c

6 files changed

Lines changed: 198 additions & 0 deletions

File tree

.codespellrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[codespell]
2+
ignore-words = .codespellignore
3+
ignore-multiline-regex = \{\s*/\*\s*\*\s*codespell:ignore-begin\s*\*\s*\*/\s*\}\s*.*?\s*\{\s*/\*\s*\*\s*codespell:ignore-end\s*\*\s*\*/\s*\}

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ jobs:
4747
rails-paradedb.gemspec \
4848
Rakefile
4949
50+
- name: Check API coverage
51+
run: ruby scripts/check_api_coverage.rb
52+
53+
- name: Gem install smoke test
54+
run: bash scripts/smoke_gem_install.sh
55+
5056
matrix-tests:
5157
name: Ruby ${{ matrix.ruby-version }} / Rails ${{ matrix.rails-version }}
5258
runs-on: ubuntu-latest

.pre-commit-config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ repos:
2929
hooks:
3030
- id: markdownlint
3131

32+
- repo: https://github.com/codespell-project/codespell
33+
rev: v2.4.1
34+
hooks:
35+
- id: codespell
36+
args: ["--config", ".codespellrc", "--check-filenames"]
37+
3238
- repo: local
3339
hooks:
3440
- id: ruby-syntax

CONTRIBUTING.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ prek install -f
7070

7171
If you change Ruby code, keep style consistent with existing files and tests.
7272

73+
### API and Packaging Consistency Checks
74+
75+
Run these checks before opening a PR when you change API wrappers or packaging:
76+
77+
```bash
78+
ruby scripts/check_api_coverage.rb
79+
bash scripts/smoke_gem_install.sh
80+
```
81+
7382
### Pull Request Workflow
7483

7584
1. Ensure your change has an issue.

scripts/check_api_coverage.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "json"
5+
require "pathname"
6+
require "set"
7+
8+
ROOT = Pathname(__dir__).join("..").expand_path
9+
API_JSON = ROOT.join("api.json")
10+
APIIGNORE_JSON = ROOT.join("apiignore.json")
11+
TOKENIZER_TYPE_KEY_PREFIX = "PDB_TYPE_TOKENIZER_"
12+
13+
def load_json(path)
14+
JSON.parse(path.read)
15+
rescue Errno::ENOENT
16+
raise "#{path} not found"
17+
rescue JSON::ParserError => e
18+
raise "invalid JSON in #{path}: #{e.message}"
19+
end
20+
21+
def flatten_ignore(section, kind:)
22+
case section
23+
when nil
24+
Set.new
25+
when Array
26+
Set.new(section.map(&:to_s))
27+
when Hash
28+
Set.new(section.values.flatten.map(&:to_s))
29+
else
30+
raise "apiignore #{kind} section must be an Array or object of Arrays"
31+
end
32+
end
33+
34+
def source_paths
35+
files = Dir.glob(ROOT.join("lib/**/*.rb").to_s).sort
36+
rakefile = ROOT.join("Rakefile")
37+
files << rakefile.to_s if rakefile.file?
38+
files
39+
end
40+
41+
def read_sources
42+
source_paths.to_h { |path| [path, File.read(path)] }
43+
end
44+
45+
def missing_quoted_tokens(expected_tokens, source_text)
46+
expected_tokens.select do |token|
47+
!source_text.match?(/["']#{Regexp.escape(token)}["']/)
48+
end.sort
49+
end
50+
51+
def missing_symbols(expected_symbols, source_text)
52+
expected_symbols.select { |symbol| !source_text.include?(symbol) }.sort
53+
end
54+
55+
def main
56+
api = load_json(API_JSON)
57+
apiignore = APIIGNORE_JSON.file? ? load_json(APIIGNORE_JSON) : {}
58+
59+
operators = Set.new(api.fetch("operators").values.map(&:to_s))
60+
functions = Set.new(api.fetch("functions").values.map(&:to_s))
61+
all_types_by_key = api.fetch("types").transform_keys(&:to_s).transform_values(&:to_s)
62+
63+
tokenizer_types = Set.new(
64+
all_types_by_key
65+
.select { |name, _| name.start_with?(TOKENIZER_TYPE_KEY_PREFIX) }
66+
.values
67+
)
68+
static_types = Set.new(
69+
all_types_by_key
70+
.reject { |name, _| name.start_with?(TOKENIZER_TYPE_KEY_PREFIX) }
71+
.values
72+
)
73+
74+
ignored_operators = flatten_ignore(apiignore["operators"], kind: "operators")
75+
ignored_functions = flatten_ignore(apiignore["functions"], kind: "functions")
76+
ignored_types = flatten_ignore(apiignore["types"], kind: "types")
77+
78+
sources = read_sources
79+
source_text = sources.values.join("\n")
80+
81+
missing_ops = missing_quoted_tokens(operators, source_text)
82+
missing_functions = missing_symbols(functions, source_text)
83+
missing_static_types = missing_symbols(static_types, source_text)
84+
85+
tokenizer_sql = sources.find { |path, _| path.end_with?("lib/parade_db/tokenizer_sql.rb") }&.last.to_s
86+
dynamic_tokenizer_supported = tokenizer_sql.include?('"pdb.#{function_name}"')
87+
missing_tokenizer_types = dynamic_tokenizer_supported ? [] : tokenizer_types.to_a.sort
88+
89+
referenced_symbols = Set.new(source_text.scan(/\bpdb\.[a-zA-Z_][a-zA-Z0-9_]*\b/))
90+
allowed_symbols = functions | static_types | tokenizer_types | ignored_functions | ignored_types
91+
untracked_symbols = (referenced_symbols - allowed_symbols).to_a.sort
92+
93+
issues = []
94+
95+
unless missing_ops.empty?
96+
issues << "operators declared in api.json but not found in Ruby wrappers: #{missing_ops.join(', ')}"
97+
end
98+
99+
unless missing_functions.empty?
100+
issues << "functions declared in api.json but not found in Ruby wrappers: #{missing_functions.join(', ')}"
101+
end
102+
103+
unless missing_static_types.empty?
104+
issues << "types declared in api.json but not found in Ruby wrappers: #{missing_static_types.join(', ')}"
105+
end
106+
107+
unless missing_tokenizer_types.empty?
108+
issues << "tokenizer types require dynamic tokenizer qualification, but support was not detected: #{missing_tokenizer_types.join(', ')}"
109+
end
110+
111+
unless untracked_symbols.empty?
112+
issues << "pdb.* symbols used in code but missing from api.json/apiignore.json: #{untracked_symbols.join(', ')}"
113+
end
114+
115+
if issues.empty?
116+
puts "✅ API coverage check passed."
117+
puts " operators: #{operators.size}, functions: #{functions.size}, types: #{all_types_by_key.size}, referenced symbols: #{referenced_symbols.size}"
118+
return 0
119+
end
120+
121+
warn "❌ API coverage check failed:"
122+
issues.each { |issue| warn " - #{issue}" }
123+
warn "\nUpdate api.json, apiignore.json, or wrapper usage so they stay in sync."
124+
1
125+
end
126+
127+
exit(main)

scripts/smoke_gem_install.sh

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5+
cd "${ROOT_DIR}"
6+
7+
VERSION="$(ruby -e 'require File.expand_path("lib/parade_db/version", Dir.pwd); print ParadeDB::VERSION')"
8+
GEM_FILE="rails-paradedb-${VERSION}.gem"
9+
10+
rm -f "${GEM_FILE}"
11+
gem build rails-paradedb.gemspec >/dev/null
12+
13+
if [[ ! -f "${GEM_FILE}" ]]; then
14+
echo "❌ Expected gem file not found after build: ${GEM_FILE}" >&2
15+
exit 1
16+
fi
17+
18+
SMOKE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/rails-paradedb-smoke.XXXXXX")"
19+
GEM_HOME_DIR="${SMOKE_DIR}/gem-home"
20+
HOME_DIR="${SMOKE_DIR}/home"
21+
22+
cleanup() {
23+
rm -rf "${SMOKE_DIR}" "${GEM_FILE}"
24+
}
25+
trap cleanup EXIT
26+
27+
mkdir -p "${GEM_HOME_DIR}" "${HOME_DIR}"
28+
export HOME="${HOME_DIR}"
29+
export GEM_SPEC_CACHE="${SMOKE_DIR}/spec-cache"
30+
31+
gem install --no-document --install-dir "${GEM_HOME_DIR}" "${GEM_FILE}" >/dev/null
32+
33+
EXPECTED_VERSION="${VERSION}" GEM_HOME="${GEM_HOME_DIR}" GEM_PATH="${GEM_HOME_DIR}" ruby <<'RUBY'
34+
require "parade_db"
35+
require "arel"
36+
37+
expected = ENV.fetch("EXPECTED_VERSION")
38+
abort("Version mismatch: expected #{expected}, got #{ParadeDB::VERSION}") unless ParadeDB::VERSION == expected
39+
40+
builder = ParadeDB::Arel::Builder.new("mock_items")
41+
node = builder.match(:description, "running shoes")
42+
unless node.is_a?(Arel::Nodes::InfixOperation) && node.operator.to_s == "&&&"
43+
abort("Generated node is not a ParadeDB match infix operation")
44+
end
45+
46+
puts "✅ Gem smoke install passed for rails-paradedb #{ParadeDB::VERSION}"
47+
RUBY

0 commit comments

Comments
 (0)