riteway 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6761c022b9dd95c5940eb0d3b373306d29aec20ef8b1b952bbe56e13f148fdaf
4
+ data.tar.gz: 4583be719e30bfaaa0d2174e727b07c647125681c552e032eb26beffda272670
5
+ SHA512:
6
+ metadata.gz: efc1d4c26e6c2bb6660543b6106bc6853db6e4508ade85ea1a9ff3eaa25eb79771e314dcbcacde64971caaf5a131c5d1d6d26f7bf83bf0a1a9b5827c32ad9341
7
+ data.tar.gz: 7ad168dea1489678eebbb73fdef53ae15a1777f8e835ac816100045fa6dacf10407aebf6e00eb96b1fbb795e876d85b8c547593d5ea3bcbd009a3961ac89ff28
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-03-11
11
+
12
+ ### Added
13
+
14
+ - `Riteway.assert` with RSpec and Minitest adapters
15
+ - `Riteway.attempt` for testing error cases
16
+ - `Riteway.count_keys` convenience helper
17
+ - `Riteway.match` curried text search
18
+
19
+ [unreleased]: https://github.com/mycargus/riteway-ruby/compare/v0.1.0...HEAD
20
+ [0.1.0]: https://github.com/mycargus/riteway-ruby/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael Hargiss
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # Riteway Ruby
2
+
3
+ [![CI](https://github.com/mycargus/riteway-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/mycargus/riteway-ruby/actions/workflows/ci.yml)
4
+
5
+ **The standard testing assertion style for AI Driven Development (AIDD) and software agents.**
6
+
7
+ This is the Ruby port of the [riteway JavaScript library](https://github.com/paralleldrive/riteway).
8
+
9
+ Riteway is a testing assertion style and philosophy which leads to simple, readable, helpful unit tests for humans and AI agents.
10
+
11
+ It lets you write better, more readable tests with a fraction of the code that traditional assertion frameworks would use.
12
+
13
+ Riteway is the AI-native way to build a modern test suite. It pairs well with RSpec, Claude Code, Cursor Agent, and more.
14
+
15
+ * **R**eadable
16
+ * **I**solated/**I**ntegrated
17
+ * **T**horough
18
+ * **E**xplicit
19
+
20
+ Riteway forces you to write **R**eadable, **I**solated, and **E**xplicit tests, because that's the only way you can use the API. It also makes it easier to be thorough by making test assertions so simple that you'll want to write more of them.
21
+
22
+ ## Why Riteway for AI Driven Development?
23
+
24
+ Riteway's structured approach makes it ideal for AIDD:
25
+
26
+ **📖 Learn more:** [Better AI Driven Development with Test Driven Development](https://medium.com/effortless-programming/better-ai-driven-development-with-test-driven-development-d4849f67e339)
27
+
28
+ - **Clear requirements**: The given/should structure and 5-question framework help AI better understand exactly what to build
29
+ - **Readable by design**: Natural language descriptions make tests comprehensible to both humans and AI
30
+ - **Simple API**: Minimal surface area reduces AI confusion and hallucinations
31
+ - **Token efficient**: Concise syntax saves valuable context window space
32
+
33
+ ## The 5 Questions Every Test Must Answer
34
+
35
+ There are [5 questions every unit test must answer](https://medium.com/javascript-scene/what-every-unit-test-needs-f6cd34d9836d). Riteway forces you to answer them.
36
+
37
+ 1. What is the unit under test (module, function, class, whatever)?
38
+ 2. What should it do? (Prose description)
39
+ 3. What was the actual output?
40
+ 4. What was the expected output?
41
+ 5. How do you reproduce the failure?
42
+
43
+ ## Installing
44
+
45
+ Add to your `Gemfile`:
46
+
47
+ ```ruby
48
+ gem "riteway"
49
+ ```
50
+
51
+ Or install directly:
52
+
53
+ ```shell
54
+ gem install riteway
55
+ ```
56
+
57
+ Then require the adapter for your test framework. Use one or the other — not both.
58
+
59
+ **RSpec** — require in `spec/spec_helper.rb`:
60
+
61
+ ```ruby
62
+ require "riteway/rspec"
63
+ ```
64
+
65
+ Optional: filter Riteway internals from RSpec backtraces so failures point directly to your test code:
66
+
67
+ ```ruby
68
+ RSpec.configure do |config|
69
+ config.backtrace_exclusion_patterns << /lib\/riteway/
70
+ end
71
+ ```
72
+
73
+ **Minitest** — require in `test/test_helper.rb`:
74
+
75
+ ```ruby
76
+ require "minitest/autorun"
77
+ require "riteway/minitest"
78
+ ```
79
+
80
+ Minitest ships with Ruby's standard library — no extra gem needed.
81
+
82
+ ## Example Usage
83
+
84
+ ```ruby
85
+ require "riteway/rspec"
86
+
87
+ # A function to test
88
+ def sum(*args)
89
+ args.each { |n| raise TypeError, "Not a number: #{n.inspect}" unless n.is_a?(Numeric) }
90
+ args.reduce(0, :+)
91
+ end
92
+
93
+ RSpec.describe "sum()" do
94
+ it "given no arguments, should return 0" do
95
+ Riteway.assert(
96
+ given: "no arguments",
97
+ should: "return 0",
98
+ actual: sum(),
99
+ expected: 0
100
+ )
101
+ end
102
+
103
+ it "given zero, should return the correct sum" do
104
+ Riteway.assert(
105
+ given: "zero",
106
+ should: "return the correct sum",
107
+ actual: sum(2, 0),
108
+ expected: 2
109
+ )
110
+ end
111
+
112
+ it "given negative numbers, should return the correct sum" do
113
+ Riteway.assert(
114
+ given: "negative numbers",
115
+ should: "return the correct sum",
116
+ actual: sum(1, -4),
117
+ expected: -3
118
+ )
119
+ end
120
+
121
+ it "given a non-numeric argument, should raise TypeError" do
122
+ error = Riteway.attempt(method(:sum), 1, "NaN")
123
+
124
+ Riteway.assert(
125
+ given: "a non-numeric argument",
126
+ should: "raise TypeError",
127
+ actual: error.class,
128
+ expected: TypeError
129
+ )
130
+ end
131
+ end
132
+ ```
133
+
134
+ ## Output
135
+
136
+ Riteway uses your test framework's output. The failure message always includes the given/should context; the diff format varies slightly between RSpec (colorized diff) and Minitest (simple expected/got lines).
137
+
138
+ For RSpec, use `--format documentation` for full prose output:
139
+
140
+ ```shell
141
+ bundle exec rspec --format documentation
142
+ ```
143
+
144
+ ```
145
+ sum()
146
+ given no arguments, should return 0
147
+ given zero, should return the correct sum
148
+ given negative numbers, should return the correct sum
149
+ given a non-numeric argument, should raise TypeError
150
+
151
+ Finished in 0.00112 seconds
152
+ 4 examples, 0 failures
153
+ ```
154
+
155
+ When a test fails, the given/should context is always included in the error:
156
+
157
+ ```
158
+ Given negative numbers: should return the correct sum
159
+ expected: -3
160
+ got: 0
161
+ ```
162
+
163
+ ## API
164
+
165
+ ### `Riteway.assert`
166
+
167
+ ```ruby
168
+ Riteway.assert(given:, should:, actual:, expected:) => void, raises
169
+ ```
170
+
171
+ The core assertion. Takes keyword arguments and compares `actual` to `expected` using deep equality (`eq`). All four arguments are required — missing any raises Ruby's native `ArgumentError`.
172
+
173
+ `assert` uses RSpec's `eq` matcher, which handles deep comparison of arrays, hashes, and nested structures.
174
+
175
+ ```ruby
176
+ Riteway.assert(
177
+ given: "an array of numbers",
178
+ should: "equal the expected array",
179
+ actual: [1, 2, 3].map { |n| n * 2 },
180
+ expected: [2, 4, 6]
181
+ )
182
+ ```
183
+
184
+ ### `Riteway.attempt`
185
+
186
+ ```ruby
187
+ Riteway.attempt(callable = nil, *args, **kwargs, &block) => Error | Any
188
+ ```
189
+
190
+ Execute a callable or block with the given arguments. Returns the error if one is raised, otherwise returns the result. Designed for testing error cases in your assertions. Supports positional args, keyword args, lambdas, procs, and blocks.
191
+
192
+ `attempt` catches `StandardError` and its subclasses only. Exceptions outside this hierarchy — `SystemExit`, `Interrupt`, `SignalException` — propagate normally. Note that RSpec's `ExpectationNotMetError` inherits from `Exception` (not `StandardError`), so it propagates through `attempt` rather than being caught and returned.
193
+
194
+ ```ruby
195
+ # Block form (most concise)
196
+ error = Riteway.attempt { Integer("not a number") }
197
+
198
+ # Lambda form
199
+ error = Riteway.attempt(-> { Integer("not a number") })
200
+
201
+ # Method reference with positional args
202
+ error = Riteway.attempt(method(:sum), 1, "NaN")
203
+
204
+ # Method reference with keyword args
205
+ result = Riteway.attempt(method(:create_user), name: "Alice", age: 30)
206
+
207
+ Riteway.assert(
208
+ given: "a non-numeric string",
209
+ should: "raise ArgumentError",
210
+ actual: error.class,
211
+ expected: ArgumentError
212
+ )
213
+ ```
214
+
215
+ ### `Riteway.count_keys`
216
+
217
+ ```ruby
218
+ Riteway.count_keys(hash = {}) => Integer
219
+ ```
220
+
221
+ Given a hash, return a count of its keys. Defaults to `{}` (returns `0`) when called with no arguments. A convenience wrapper for `hash.keys.length` that reads naturally inside a `Riteway.assert` call. Handy when you're adding new state to a hash keyed by ID and want to ensure the correct number of keys were added.
222
+
223
+ ```ruby
224
+ Riteway.assert(
225
+ given: "a hash with 3 keys",
226
+ should: "return 3",
227
+ actual: Riteway.count_keys({ a: 1, b: 2, c: 3 }),
228
+ expected: 3
229
+ )
230
+ ```
231
+
232
+ ### `Riteway.match`
233
+
234
+ ```ruby
235
+ Riteway.match(text) => ->(pattern) => String
236
+ ```
237
+
238
+ Take some text to search and return a lambda which takes a pattern and returns the matched text, or `nil` if no match — consistent with Ruby's own `String#match`. The pattern can be a String or Regexp. String patterns are auto-escaped so regex meta-characters are treated as literals.
239
+
240
+ ```ruby
241
+ contains = Riteway.match("<h1>Dialog Title</h1>")
242
+
243
+ Riteway.assert(
244
+ given: "some text and a string pattern",
245
+ should: "return the matched text",
246
+ actual: contains.call("Dialog Title"),
247
+ expected: "Dialog Title"
248
+ )
249
+
250
+ Riteway.assert(
251
+ given: "some text and a regex pattern",
252
+ should: "return the matched text",
253
+ actual: contains.call(/\w+/),
254
+ expected: "Dialog"
255
+ )
256
+
257
+ Riteway.assert(
258
+ given: "a pattern that does not match",
259
+ should: "return nil",
260
+ actual: contains.call("not found"),
261
+ expected: nil
262
+ )
263
+ ```
264
+
265
+ You can also use `contains.("pattern")` or `contains["pattern"]` as shorthand for `.call`.
266
+
267
+ ## Publishing
268
+
269
+ See [RELEASING.md](RELEASING.md) for the full release process.
270
+
271
+ `rake release` is intentionally disabled to prevent automated publishing.
272
+
273
+ ## License
274
+
275
+ MIT
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riteway
4
+ # Returns a lambda that searches text for a pattern (String or Regexp).
5
+ # Returns the matched text on success, or nil if no match — consistent with
6
+ # Ruby's String#match which also returns nil on no match.
7
+ def self.match(text)
8
+ raise TypeError, "match expects a String, got #{text.class}" unless text.is_a?(String)
9
+
10
+ ->(pattern) {
11
+ unless pattern.is_a?(String) || pattern.is_a?(Regexp)
12
+ raise TypeError,
13
+ "pattern must be a String or Regexp, got #{pattern.class}"
14
+ end
15
+ raise ArgumentError, "pattern must not be empty" if pattern.is_a?(String) && pattern.empty?
16
+
17
+ re = pattern.is_a?(String) ? Regexp.new(Regexp.escape(pattern)) : pattern
18
+ matched = text.match(re)
19
+ matched ? matched[0] : nil
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "riteway"
5
+
6
+ module Riteway
7
+ if defined?(ADAPTER)
8
+ raise LoadError,
9
+ "riteway: adapter conflict — #{ADAPTER} already loaded. " \
10
+ "Only require one adapter (riteway/rspec or riteway/minitest)."
11
+ end
12
+
13
+ ADAPTER = :minitest
14
+
15
+ module MinitestLifecycle
16
+ def before_setup
17
+ super
18
+ Thread.current[:riteway_minitest_context] = self
19
+ end
20
+
21
+ def after_teardown
22
+ Thread.current[:riteway_minitest_context] = nil
23
+ super
24
+ end
25
+ end
26
+
27
+ def self.assert(given:, should:, actual:, expected:)
28
+ ctx = Thread.current[:riteway_minitest_context]
29
+ unless ctx
30
+ raise "Riteway.assert must be called inside an it/test block. " \
31
+ "Ensure `require \"riteway/minitest\"` is in your test_helper.rb " \
32
+ "and that assert is only called from within a test context."
33
+ end
34
+ unless given.is_a?(String) && !given.empty?
35
+ raise ArgumentError,
36
+ "given: must be a non-empty String, got #{given.inspect}"
37
+ end
38
+ unless should.is_a?(String) && !should.empty?
39
+ raise ArgumentError,
40
+ "should: must be a non-empty String, got #{should.inspect}"
41
+ end
42
+
43
+ message = "Given #{given}: should #{should}"
44
+ expected.nil? ? ctx.assert_nil(actual, message) : ctx.assert_equal(expected, actual, message)
45
+ nil
46
+ end
47
+ end
48
+
49
+ Minitest::Test.include Riteway::MinitestLifecycle
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "rspec/expectations"
5
+ require "rspec/matchers"
6
+ rescue LoadError
7
+ raise LoadError,
8
+ "riteway/rspec requires the 'rspec' gem. Add `gem \"rspec\"` to your Gemfile, " \
9
+ "or use `require \"riteway/minitest\"` for Minitest."
10
+ end
11
+ require "riteway"
12
+
13
+ module Riteway
14
+ if defined?(ADAPTER)
15
+ raise LoadError,
16
+ "riteway: adapter conflict — #{ADAPTER} already loaded. " \
17
+ "Only require one adapter (riteway/rspec or riteway/minitest)."
18
+ end
19
+
20
+ ADAPTER = :rspec
21
+
22
+ # Internal — not part of the public API. Isolates RSpec matcher methods
23
+ # so they don't pollute Riteway's module namespace.
24
+ module RSpecBridge
25
+ extend RSpec::Matchers
26
+ end
27
+
28
+ def self.assert(given:, should:, actual:, expected:)
29
+ unless RSpec.respond_to?(:current_example) && RSpec.current_example
30
+ raise "Riteway.assert must be called inside an it/specify block, not at describe-level. " \
31
+ "Move this assertion inside an `it` block."
32
+ end
33
+ unless given.is_a?(String) && !given.empty?
34
+ raise ArgumentError,
35
+ "given: must be a non-empty String, got #{given.inspect}"
36
+ end
37
+ unless should.is_a?(String) && !should.empty?
38
+ raise ArgumentError,
39
+ "should: must be a non-empty String, got #{should.inspect}"
40
+ end
41
+
42
+ matcher = RSpecBridge.eq(expected)
43
+ return if matcher.matches?(actual)
44
+
45
+ raise RSpec::Expectations::ExpectationNotMetError,
46
+ "Given #{given}: should #{should}\n#{matcher.failure_message}"
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Riteway
4
+ VERSION = "0.1.0"
5
+ end
data/lib/riteway.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "riteway/version"
4
+ require_relative "riteway/match"
5
+
6
+ module Riteway
7
+ def self.assert(**)
8
+ raise "Riteway.assert requires an adapter. " \
9
+ "Add `require \"riteway/rspec\"` or `require \"riteway/minitest\"` to your test helper."
10
+ end
11
+
12
+ # Calls callable (or block) with given args. Returns the error if raised,
13
+ # otherwise returns the result. Catches StandardError and subclasses only —
14
+ # SystemExit, Interrupt, SignalException, etc. propagate normally.
15
+ def self.attempt(callable = nil, *args, **kwargs, &block)
16
+ raise ArgumentError, "attempt accepts a callable or a block, not both" if callable && block
17
+
18
+ fn = callable || block
19
+ raise ArgumentError, "attempt requires a callable or a block" unless fn
20
+ raise ArgumentError, "attempt expects a callable (responds to #call), got #{fn.class}" unless fn.respond_to?(:call)
21
+
22
+ begin
23
+ kwargs.empty? ? fn.call(*args) : fn.call(*args, **kwargs)
24
+ rescue => error
25
+ error
26
+ end
27
+ end
28
+
29
+ def self.count_keys(hash = {})
30
+ raise TypeError, "count_keys expects a Hash, got #{hash.class}" unless hash.is_a?(Hash)
31
+
32
+ hash.keys.length
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: riteway
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael Hargiss
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec-expectations
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: minitest
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '5.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '5.0'
82
+ description: Ruby port of the riteway JavaScript testing library.
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - CHANGELOG.md
88
+ - LICENSE
89
+ - README.md
90
+ - lib/riteway.rb
91
+ - lib/riteway/match.rb
92
+ - lib/riteway/minitest.rb
93
+ - lib/riteway/rspec.rb
94
+ - lib/riteway/version.rb
95
+ homepage: https://github.com/mycargus/riteway-ruby
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ allowed_push_host: https://rubygems.org
100
+ homepage_uri: https://github.com/mycargus/riteway-ruby
101
+ source_code_uri: https://github.com/mycargus/riteway-ruby
102
+ changelog_uri: https://github.com/mycargus/riteway-ruby/blob/main/CHANGELOG.md
103
+ bug_tracker_uri: https://github.com/mycargus/riteway-ruby/issues
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '3.0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.6.9
119
+ specification_version: 4
120
+ summary: Unit tests that always supply a good bug report when they fail.
121
+ test_files: []