Skip to content

Commit 82db09b

Browse files
pviticlaude
andcommitted
build: add a style gate calibrated to the code that exists
Neither Ruby repo in this project had a linter, so my own Ruby this week was never checked by one. This adds rubocop to the SDK and wires it into `rake`, which is what CI runs — so `rake` locally says exactly what CI will. Straight rubocop reported 1510 offences, and the shape of that number is the point: 1139 were Style/StringLiterals, i.e. the default prefers single quotes and this project uses double quotes everywhere, consistently. Following the default would have rewritten 1139 strings to settle a preference the project had already settled. So .rubocop.yml calibrates the cops to the existing style, and every relaxation carries its reason: double quotes, table-aligned hashes where a literal IS a table, long lines allowed in comments (a copyable usage example beats a wrapped one) and in the data tables, metrics raised where the shape is the SDK's rather than a preference. Four cops are off because they are wrong about THIS code, not in general: - Naming/VariableNumber, either way round: the numbers are protocol identifiers and each spells itself its own way — retry_on_429 for the HTTP status, eip712 and secp256k1 for the standards. Enforcing one style renames the other half, and 62 offences appeared when I tried. - Style/NumericPredicate, which the SPECS caught: its autocorrection turned two nil-safe `== 0` guards into `.zero?`, and chain_id is nil when the caller wants every chain. Six specs went red on a NoMethodError. - RSpec/DescribedClass and the spec-layout cops: the spec directory is flat and mirrors the SDK's surface, and the specs name the class under test because that answers "what is this testing?" on the first line. Three findings were real and are now documented where they live, with inline disables: `method` as a Struct member (it is the HTTP method, and the shadowed Struct#method is unused), the EIP-712 preimage built with `+` (binary concatenation is what the standard defines), and the two constants referenced in void context in the SIWE loader (referencing them IS the point — it forces the autoload while warnings are muted). Verified both directions: `rake` is green, and introducing a single-quoted string makes it fail on that line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1799883 commit 82db09b

31 files changed

Lines changed: 404 additions & 114 deletions

.rubocop.yml

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Style gate for the SDK, added long after the code — so this file calibrates the cops to
2+
# the style the code already uses consistently, and leaves enabled only the ones that say
3+
# something. A linter that fights the codebase teaches people to ignore it.
4+
#
5+
# Every relaxation below is a decision with a reason. The cops NOT listed are on.
6+
require:
7+
- rubocop-rspec
8+
9+
AllCops:
10+
# The plugin nudge is noise in CI output; the extensions we want are in the Gemfile.
11+
SuggestExtensions: false
12+
# Matches the gemspec, which is the contract — not the newest thing that happens to work.
13+
TargetRubyVersion: 3.0
14+
NewCops: enable
15+
Exclude:
16+
- "vendor/**/*"
17+
- "tmp/**/*"
18+
# A build script that writes SDK files: not shipped, and shaped by what it emits.
19+
- "gen/**/*"
20+
21+
# The project writes double quotes everywhere, deliberately and without exception. Rubocop
22+
# defaults to single; following the default would have rewritten 1139 strings to settle a
23+
# preference the project had already settled.
24+
Style/StringLiterals:
25+
EnforcedStyle: double_quotes
26+
27+
# Hashes are aligned in columns where that makes a literal readable (a table of chain →
28+
# address) and key-aligned elsewhere. Both are intentional, so both are allowed.
29+
Layout/HashAlignment:
30+
EnforcedColonStyle:
31+
- key
32+
- table
33+
EnforcedHashRocketStyle:
34+
- key
35+
- table
36+
37+
# An rspec `describe` is a block, and a long one is a well-covered subject rather than a
38+
# smell. Same for the gemspec.
39+
Metrics/BlockLength:
40+
# 30, for the HTTP retry loop: one `loop do` that owns attempt counting, backoff, the
41+
# Retry-After honouring and cancellation. Splitting it would hide the sequence.
42+
Max: 30
43+
Exclude:
44+
- "spec/**/*"
45+
- "*.gemspec"
46+
47+
# The SDK's shape sets these, not a preference: a resource method mirrors an endpoint, so a
48+
# request builder with eight optional filters has eight parameters, and splitting it into
49+
# objects would make the SDK less like the API it wraps.
50+
Metrics/ParameterLists:
51+
Max: 10
52+
CountKeywordArgs: false
53+
54+
# Signing and error decoding are long by nature: a single EIP-712 payload or a revert
55+
# decoder is one idea that happens to take thirty lines, and cutting it into named halves
56+
# that are each called once would spread it rather than simplify it.
57+
Metrics/MethodLength:
58+
Max: 30
59+
Metrics/AbcSize:
60+
Max: 25
61+
Metrics/CyclomaticComplexity:
62+
Max: 12
63+
Metrics/PerceivedComplexity:
64+
Max: 12
65+
Metrics/ClassLength:
66+
Max: 250
67+
Metrics/ModuleLength:
68+
Max: 250
69+
Exclude:
70+
- "spec/**/*"
71+
72+
# Chain ids and amounts are written the way they appear on chain and in the gateway's
73+
# seeds — 84532, not 84_532 — so a value can be grepped across repos.
74+
Style/NumericLiterals:
75+
Enabled: false
76+
77+
# A spec's `let` blocks legitimately define constants for the example group.
78+
Lint/ConstantDefinitionInBlock:
79+
Exclude:
80+
- "spec/**/*"
81+
82+
# Documentation is in prose above the interesting methods, not as a mandatory class banner.
83+
Style/Documentation:
84+
Enabled: false
85+
86+
# RSpec's own opinions about example length and count say nothing about whether the tests
87+
# are good; the ones about structure are kept.
88+
RSpec/ExampleLength:
89+
Enabled: false
90+
RSpec/MultipleExpectations:
91+
Enabled: false
92+
RSpec/NestedGroups:
93+
Max: 4
94+
95+
# ── RSpec structure: the project's, not the plugin's ─────────────────────────
96+
# The specs name the class under test explicitly. `described_class` saves a line and costs
97+
# a reader the answer to "what is this testing?" — worth it in a long file, not in these.
98+
RSpec/DescribedClass:
99+
Enabled: false
100+
101+
# The spec directory is FLAT (client_spec.rb, signing_spec.rb, analytics_spec.rb) and mirrors
102+
# the SDK's surface rather than its file tree, so a reader looking for "how do I use
103+
# analytics" finds one file. Requiring spec/rail0/resources/analytics_spec.rb would nest
104+
# every file one deep to satisfy a naming rule.
105+
RSpec/SpecFilePathFormat:
106+
Enabled: false
107+
108+
# Some specs describe a behaviour rather than a class — the load path, the default logger's
109+
# output — and naming a string subject is the honest way to say so.
110+
RSpec/DescribeClass:
111+
Enabled: false
112+
113+
# A constant inside an example group is scoped to that group by intent: a fixture address or
114+
# a payload shape belongs to the examples that use it, not to the file.
115+
RSpec/LeakyConstantDeclaration:
116+
Exclude:
117+
- "spec/**/*"
118+
119+
# One file per subject, and a subject can have two aspects worth their own describe.
120+
RSpec/MultipleDescribes:
121+
Enabled: false
122+
123+
# `x.zero?` RAISES on nil where `x == 0` does not, and both places this cop rewrote compare
124+
# an OPTIONAL value: `chain_id` is nil when the caller wants every chain. The autocorrection
125+
# turned two nil-safe guards into NoMethodError, which six specs caught — the cop is right
126+
# about style and wrong about this code.
127+
Style/NumericPredicate:
128+
Enabled: false
129+
130+
# Off, because neither style is right for this code: the numbers in these names are PROTOCOL
131+
# identifiers, and each protocol has its own conventional spelling — `retry_on_429` for the
132+
# HTTP status, `eip712` and `secp256k1` for the standards, which is how they are written
133+
# everywhere including their own specifications. Enforcing either style renames the other
134+
# half, and the name is carrying the protocol, not a counter.
135+
Naming/VariableNumber:
136+
Enabled: false
137+
138+
# 120, with three kinds of exception — and ONE block, because a second `Layout/LineLength:`
139+
# key silently replaces the first in YAML rather than merging with it.
140+
#
141+
# - comments: a usage example a reader can copy is worth more than a wrap, and wrapping a
142+
# one-line `client.payments.create(...)` makes it uncopyable;
143+
# - data tables: stablecoins, error hints and type aliases are one row per line by design,
144+
# and a table is easier to scan than the same rows folded;
145+
# - specs: a stub URL or an expected payload wrapped across lines is harder to read than
146+
# the long line it came from.
147+
Layout/LineLength:
148+
Max: 120
149+
AllowedPatterns:
150+
- "^\\s*#"
151+
Exclude:
152+
- "lib/rail0/stablecoins.rb"
153+
- "lib/rail0/error_hints.rb"
154+
- "lib/rail0/types.rb"
155+
- "spec/**/*"
156+
157+
# The HTTP layer's send-and-retry path: the branching IS the feature (status classes, retry
158+
# budgets, Retry-After, cancellation), and splitting it into halves each called once would
159+
# spread the logic rather than reduce it.
160+
Metrics/AbcSize:
161+
Max: 40

Gemfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
# frozen_string_literal: true
2+
13
source "https://rubygems.org"
24

35
gemspec
46

57
group :development do
8+
gem "eth", "~> 0.5"
69
gem "rake", "~> 13.0"
710
gem "rspec", "~> 3.13"
8-
gem "webmock", "~> 3.23"
9-
gem "eth", "~> 0.5"
1011
gem "siwe-rb", "~> 0.2"
12+
gem "webmock", "~> 3.23"
13+
# Style gate. Added late, so .rubocop.yml calibrates the cops to the code that already
14+
# exists rather than the code rubocop would prefer — see the comments there.
15+
gem "rubocop", "~> 1.66", require: false
16+
gem "rubocop-rspec", "~> 3.0", require: false
1117
end

Gemfile.lock

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ GEM
99
specs:
1010
addressable (2.9.0)
1111
public_suffix (>= 2.0.2, < 8.0)
12+
ast (2.4.3)
1213
base64 (0.3.0)
1314
bigdecimal (3.3.1)
1415
bls12-381 (0.3.1)
@@ -42,18 +43,29 @@ GEM
4243
http-2 (1.1.3)
4344
httpx (1.7.8)
4445
http-2 (>= 1.1.3)
46+
json (2.21.2)
4547
keccak (1.3.3)
4648
konstructor (1.0.2)
49+
language_server-protocol (3.17.0.6)
50+
lint_roller (1.1.0)
4751
logger (1.7.0)
4852
mini_portile2 (2.8.9)
4953
openssl (3.3.3)
54+
parallel (2.1.0)
55+
parser (3.3.12.0)
56+
ast (~> 2.4.1)
57+
racc
5058
pkg-config (1.6.5)
59+
prism (1.9.0)
5160
public_suffix (7.0.5)
61+
racc (1.8.1)
62+
rainbow (3.1.1)
5263
rake (13.4.2)
5364
rbsecp256k1 (6.0.0)
5465
mini_portile2 (~> 2.8)
5566
pkg-config (~> 1.5)
5667
rubyzip (~> 2.3)
68+
regexp_parser (2.12.0)
5769
rexml (3.4.4)
5870
rspec (3.13.2)
5971
rspec-core (~> 3.13.0)
@@ -68,12 +80,34 @@ GEM
6880
diff-lcs (>= 1.2.0, < 2.0)
6981
rspec-support (~> 3.13.0)
7082
rspec-support (3.13.7)
83+
rubocop (1.89.0)
84+
json (~> 2.3)
85+
language_server-protocol (~> 3.17.0.2)
86+
lint_roller (~> 1.1.0)
87+
parallel (>= 1.10)
88+
parser (>= 3.3.0.2)
89+
rainbow (>= 2.2.2, < 4.0)
90+
regexp_parser (>= 2.9.3, < 3.0)
91+
rubocop-ast (>= 1.49.0, < 2.0)
92+
ruby-progressbar (~> 1.7)
93+
unicode-display_width (>= 2.4.0, < 4.0)
94+
rubocop-ast (1.50.0)
95+
parser (>= 3.3.7.2)
96+
prism (~> 1.7)
97+
rubocop-rspec (3.10.2)
98+
lint_roller (~> 1.1)
99+
regexp_parser (>= 2.0)
100+
rubocop (~> 1.86, >= 1.86.2)
101+
ruby-progressbar (1.13.0)
71102
rubyzip (2.4.1)
72103
scrypt (3.1.0)
73104
ffi-compiler (>= 1.0, < 2.0)
74105
rake (~> 13)
75106
siwe-rb (0.2.0)
76107
eth (>= 0.5.11, < 1.0)
108+
unicode-display_width (3.2.0)
109+
unicode-emoji (~> 4.1)
110+
unicode-emoji (4.2.0)
77111
webmock (3.26.2)
78112
addressable (>= 2.8.0)
79113
crack (>= 0.3.2)
@@ -88,11 +122,14 @@ DEPENDENCIES
88122
rail0!
89123
rake (~> 13.0)
90124
rspec (~> 3.13)
125+
rubocop (~> 1.66)
126+
rubocop-rspec (~> 3.0)
91127
siwe-rb (~> 0.2)
92128
webmock (~> 3.23)
93129

94130
CHECKSUMS
95131
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
132+
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
96133
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
97134
bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218
98135
bls12-381 (0.3.1) sha256=d1525cd319c53f14178d54f355dfe1b4c572fbf5625965c1afccdc425b9896dc
@@ -109,25 +146,40 @@ CHECKSUMS
109146
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
110147
http-2 (1.1.3) sha256=1b2f379d35a11dbae94f8a1a52c053d8c161eb4a0c98b5d1605ff1b2bf171c9c
111148
httpx (1.7.8) sha256=6d769465ed608287a272ba0e4700fc22cee6f0335d80bd5c2effaf7fb7bd2a3a
149+
json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
112150
keccak (1.3.3) sha256=970dcb1e78b8c3129ba2baff8a5baa5cebf391136db1f4018e8ccc9130794557
113151
konstructor (1.0.2) sha256=fd6ac9eb1dadc1e520e06042aa2ef0122d6a070e06cde86db5a54c0c7bfe2e31
152+
language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
153+
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
114154
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
115155
mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289
116156
openssl (3.3.3) sha256=d46902138f2987c13122fab826030a11c2bb9b8a16394215cbfc5062c5e2d335
157+
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
158+
parser (3.3.12.0) sha256=21a6d7f755d5a24dfbdc6e6b772e4e879a52e7631a88bc5a3a134606052c9828
117159
pkg-config (1.6.5) sha256=33f9f81c5322983d22b439b8b672f27777b406fea23bfec74ff14bbeb42ec733
160+
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
118161
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
162+
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
119163
rail0 (1.2.0)
164+
rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
120165
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
121166
rbsecp256k1 (6.0.0) sha256=d38f563bfc6bcf3ce20c336a4798cc6990427f291e74c6817360363a72f11669
167+
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
122168
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
123169
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
124170
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
125171
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
126172
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
127173
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
174+
rubocop (1.89.0) sha256=4dee8e3ee9c45e474834efd9e8d6fd031e8331c8dacdff0de4ad65ae0a6faae7
175+
rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db
176+
rubocop-rspec (3.10.2) sha256=0b3e2ecc592cd10ecbf0095bb58d1e357905276e069643523cc19eb7495f65e2
177+
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
128178
rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615
129179
scrypt (3.1.0) sha256=67fde35bc7e3b7fe906c5be9c242acf95fe4a886c89572f405f6b11df30aa6af
130180
siwe-rb (0.2.0) sha256=f470ad513f7c85641c76dcd1e6a45867c8c93d8383ddd3feda7549606966c3d3
181+
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
182+
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
131183
webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
132184

133185
BUNDLED WITH

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,13 +642,24 @@ lib/rail0/
642642

643643
```bash
644644
bundle install
645-
bundle exec rake # run the test suite (default task)
645+
bundle exec rake # rubocop, then the specs — the same gate CI runs
646+
647+
bundle exec rubocop # style only
648+
bundle exec rubocop -a # and fix what is safely fixable
649+
bundle exec rspec # specs only
646650

647651
# Regenerate lib/rail0/types.rb after a gateway schema change:
648652
# defaults to ../rail0-gateway/docs/openapi.json, or set RAIL0_SCHEMA_PATH.
649653
ruby gen/generate.rb
650654
```
651655

656+
`.rubocop.yml` is calibrated to the code that already exists rather than to rubocop's
657+
defaults, and every relaxation in it carries the reason — double quotes because the project
658+
uses them everywhere, table-aligned hashes because some literals are tables, and
659+
`Naming/VariableNumber` off because the numbers in these names are protocol identifiers
660+
(`retry_on_429`, `eip712`, `secp256k1`) that each spell themselves their own way. A linter
661+
that argues with the codebase teaches people to ignore it.
662+
652663
## License
653664

654665
[MIT](LICENSE)

Rakefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
# frozen_string_literal: true
22

33
require "rspec/core/rake_task"
4+
require "rubocop/rake_task"
45

56
RSpec::Core::RakeTask.new(:spec)
7+
RuboCop::RakeTask.new(:rubocop)
68

7-
task default: :spec
9+
# The CI gate runs `bundle exec rake`, so both live here rather than in the workflow: a
10+
# contributor running `rake` locally gets exactly what CI will say.
11+
#
12+
# Style FIRST, deliberately: it is the faster of the two and its failures are the cheaper to
13+
# act on, so a formatting slip does not wait behind the suite.
14+
task default: %i[rubocop spec]

0 commit comments

Comments
 (0)