Skip to content

Commit a93c143

Browse files
committed
Add complete Ruby gem implementation with TypeScript improvements
Ruby Implementation: - Complete Ruby gem with garde_fou package name - Multiple calling patterns: call(), [], protect() - GuardedClient mixin for class-level protection - Comprehensive RSpec test suite (30 tests) - Ruby-idiomatic API design - Release automation scripts - Full documentation and examples TypeScript Improvements: - Python-like callable syntax: guard(fn, args) - Both direct call and explicit method patterns - Enhanced test coverage (22 tests) - Updated documentation with both calling patterns - Real-world usage examples Features: - Call counting with configurable limits - Duplicate call detection - Flexible violation handlers (warn/raise/custom) - JSON/YAML configuration support - Cross-language API consistency All three implementations (Python, TypeScript, Ruby) now have feature parity with language-specific optimizations.
1 parent 4049e80 commit a93c143

30 files changed

+1817
-123
lines changed

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,51 @@ npm install garde-fou
3636
```typescript
3737
import { GardeFou } from 'garde-fou';
3838

39-
const guard = new GardeFou({ max_calls: 5, on_violation_max_calls: 'warn' });
40-
const result = guard.call(yourApiFunction, "your", "arguments");
39+
const guard = GardeFou({ max_calls: 5, on_violation_max_calls: 'warn' });
40+
41+
// Two equivalent calling patterns:
42+
// 1. Direct call (Python-like syntax)
43+
const result = guard(yourApiFunction, "your", "arguments");
44+
45+
// 2. Explicit method call
46+
const result2 = guard.call(yourApiFunction, "your", "arguments");
4147

4248
// For async functions
4349
const asyncResult = await guard.callAsync(yourAsyncApiFunction, "args");
4450
```
4551

52+
### Ruby
53+
```bash
54+
gem install garde_fou
55+
```
56+
57+
```ruby
58+
require 'gardefou'
59+
60+
guard = Gardefou::GardeFou.new(max_calls: 5, on_violation_max_calls: 'warn')
61+
62+
# Three equivalent calling patterns:
63+
# 1. Method call
64+
result = guard.call(your_api_method, "your", "arguments")
65+
66+
# 2. Bracket syntax (Ruby callable style)
67+
result = guard[your_api_method, "your", "arguments"]
68+
69+
# 3. Protect method (semantic)
70+
result = guard.protect(your_api_method, "your", "arguments")
71+
```
72+
4673
## Repository Layout
4774

4875
- **[python/](python/)** – ✅ **Ready!** Full Python package published to PyPI
4976
- **[js/](js/)** – ✅ **Ready!** TypeScript/JavaScript package with full type support
50-
- **[ruby/](ruby/)**🚧 Ruby gem (in development)
77+
- **[ruby/](ruby/)****Ready!** Ruby gem with multiple calling patterns and mixin support
5178

5279
## Status
5380

5481
- **Python**: ✅ Complete and published to PyPI
5582
- **JavaScript/TypeScript**: ✅ Complete with TypeScript support and comprehensive test suite
56-
- **Ruby**: 🚧 Planned
83+
- **Ruby**: ✅ Complete with Ruby-idiomatic API and comprehensive RSpec test suite
5784

5885
## Contributing
5986

js/README.md

Lines changed: 120 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,47 +27,95 @@ yarn add garde-fou
2727
```typescript
2828
import { GardeFou } from 'garde-fou';
2929

30-
// Protect any function with call limits
31-
const guard = new GardeFou({ max_calls: 5, on_violation_max_calls: 'warn' });
30+
const guard = GardeFou({ max_calls: 5, on_violation_max_calls: 'warn' });
3231

33-
// Instead of: result = expensiveApiCall("query")
34-
// Use: result = guard.call(expensiveApiCall, "query")
35-
const result = guard.call(yourApiFunction, "your", "arguments");
32+
// Two equivalent ways to call your protected function:
33+
34+
// 1. Direct call (Python-like syntax) - RECOMMENDED
35+
const result = guard(yourApiFunction, "your", "arguments");
36+
37+
// 2. Explicit method call
38+
const result2 = guard.call(yourApiFunction, "your", "arguments");
39+
40+
// For async functions, use callAsync method
41+
const asyncResult = await guard.callAsync(yourAsyncApiFunction, "args");
3642
```
3743

44+
## Calling Patterns
45+
46+
garde-fou supports two calling patterns that work identically:
47+
48+
### Pattern 1: Direct Call (Recommended)
49+
```typescript
50+
const guard = GardeFou({ max_calls: 3 });
51+
52+
// Call guard directly like a function (same as Python)
53+
const result = guard(apiFunction, param1, param2);
54+
```
55+
56+
### Pattern 2: Explicit Method Call
57+
```typescript
58+
const guard = GardeFou({ max_calls: 3 });
59+
60+
// Use explicit .call() method
61+
const result = guard.call(apiFunction, param1, param2);
62+
```
63+
64+
**Both patterns:**
65+
- Share the same call count and quota
66+
- Work with duplicate detection
67+
- Have identical behavior and performance
68+
3869
## Usage Examples
3970

4071
### Basic Call Limiting
4172
```typescript
4273
import { GardeFou, QuotaExceededError } from 'garde-fou';
4374

4475
// Create a guard with a 3-call limit
45-
const guard = new GardeFou({ max_calls: 3, on_violation_max_calls: 'raise' });
76+
const guard = GardeFou({ max_calls: 3, on_violation_max_calls: 'raise' });
4677

4778
try {
48-
for (let i = 0; i < 5; i++) {
49-
const result = guard.call(apiCall, `query ${i}`);
50-
}
79+
// Using direct call syntax (recommended)
80+
guard(apiCall, 'query 1');
81+
guard(apiCall, 'query 2');
82+
guard(apiCall, 'query 3');
83+
guard(apiCall, 'query 4'); // This will throw!
5184
} catch (error) {
5285
if (error instanceof QuotaExceededError) {
5386
console.log('Call limit exceeded!');
5487
}
5588
}
89+
90+
// Alternative: using explicit method calls
91+
try {
92+
guard.call(apiCall, 'query 1');
93+
guard.call(apiCall, 'query 2');
94+
guard.call(apiCall, 'query 3');
95+
guard.call(apiCall, 'query 4'); // This will also throw!
96+
} catch (error) {
97+
console.log('Same behavior with .call()');
98+
}
5699
```
57100

58101
### Duplicate Call Detection
59102
```typescript
60103
// Warn on duplicate calls
61-
const guard = new GardeFou({ on_violation_duplicate_call: 'warn' });
104+
const guard = GardeFou({ on_violation_duplicate_call: 'warn' });
62105

63-
guard.call(apiCall, 'hello'); // First call - OK
64-
guard.call(apiCall, 'hello'); // Duplicate - Warning logged
106+
// Direct call syntax
107+
guard(apiCall, 'hello'); // First call - OK
108+
guard(apiCall, 'hello'); // Duplicate - Warning logged
109+
guard(apiCall, 'world'); // Different call - OK
110+
111+
// Explicit method syntax (works identically)
112+
guard.call(apiCall, 'hello'); // Also detected as duplicate!
65113
guard.call(apiCall, 'world'); // Different call - OK
66114
```
67115

68116
### Async Function Protection
69117
```typescript
70-
const guard = new GardeFou({ max_calls: 3, on_violation_max_calls: 'raise' });
118+
const guard = GardeFou({ max_calls: 3, on_violation_max_calls: 'raise' });
71119

72120
try {
73121
const result1 = await guard.callAsync(asyncApiCall, 'query 1');
@@ -111,10 +159,14 @@ const customHandler = (profile) => {
111159
// Send alert, log to service, etc.
112160
};
113161

114-
const guard = new GardeFou({
162+
const guard = GardeFou({
115163
max_calls: 5,
116164
on_violation_max_calls: customHandler
117165
});
166+
167+
// Both calling styles work with custom handlers
168+
guard(apiCall, 'test'); // Direct call
169+
guard.call(apiCall, 'test'); // Explicit method call
118170
```
119171

120172
## Configuration Options
@@ -168,6 +220,53 @@ interface ProfileConfig {
168220
}
169221
```
170222

223+
## Real-World Examples
224+
225+
### OpenAI API Protection
226+
```typescript
227+
import OpenAI from 'openai';
228+
import { GardeFou } from 'garde-fou';
229+
230+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
231+
const guard = GardeFou({
232+
max_calls: 100,
233+
on_violation_max_calls: 'warn',
234+
on_violation_duplicate_call: 'warn'
235+
});
236+
237+
// Before: Direct API call
238+
// const response = await openai.chat.completions.create({
239+
// model: 'gpt-4',
240+
// messages: [{ role: 'user', content: 'Hello!' }]
241+
// });
242+
243+
// After: Protected API call (choose your preferred syntax)
244+
245+
// Option 1: Direct call (Python-like)
246+
const response = await guard.callAsync(openai.chat.completions.create, {
247+
model: 'gpt-4',
248+
messages: [{ role: 'user', content: 'Hello!' }]
249+
});
250+
251+
// Option 2: Explicit method call
252+
const response2 = await guard.callAsync(openai.chat.completions.create, {
253+
model: 'gpt-4',
254+
messages: [{ role: 'user', content: 'Hello!' }]
255+
});
256+
```
257+
258+
### Multiple API Services
259+
```typescript
260+
const guard = GardeFou({ max_calls: 50 });
261+
262+
// Protect different APIs with the same guard
263+
const openaiResult = guard(openai.chat.completions.create, { /* config */ });
264+
const anthropicResult = guard(anthropic.messages.create, { /* config */ });
265+
const cohereResult = guard.call(cohere.generate, { /* config */ });
266+
267+
console.log(`Total API calls made: ${guard.profile.call_count}`);
268+
```
269+
171270
## How It Works
172271

173272
garde-fou works by wrapping your function calls. Instead of calling your API function directly, you call it through the guard:
@@ -176,8 +275,13 @@ garde-fou works by wrapping your function calls. Instead of calling your API fun
176275
// Before
177276
const result = openai.chat.completions.create({ messages: [...] });
178277

179-
// After
180-
const guard = new GardeFou({ max_calls: 10 });
278+
// After - choose your preferred syntax:
279+
const guard = GardeFou({ max_calls: 10 });
280+
281+
// Option 1: Direct call (recommended)
282+
const result = guard(openai.chat.completions.create, { messages: [...] });
283+
284+
// Option 2: Explicit method call
181285
const result = guard.call(openai.chat.completions.create, { messages: [...] });
182286
```
183287

js/examples/example.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ async function expensiveAsyncApiCall(query, model = 'gpt-4') {
1818
async function main() {
1919
console.log('=== GardeFou TypeScript Example Usage ===\n');
2020

21-
// Example 1: Basic usage with max_calls limit
21+
// Example 1: Basic usage with max_calls limit (Python-like syntax!)
2222
console.log('1. Basic usage with call limit:');
23-
const guard = new GardeFou({ max_calls: 3, on_violation_max_calls: 'warn' });
23+
const guard = GardeFou({ max_calls: 3, on_violation_max_calls: 'warn' });
2424

2525
for (let i = 0; i < 5; i++) {
2626
try {
27-
const result = guard.call(expensiveApiCall, `Query ${i + 1}`);
27+
// Now you can call guard directly like in Python!
28+
const result = guard(expensiveApiCall, `Query ${i + 1}`);
2829
console.log(` Result: ${result}`);
2930
} catch (error) {
3031
console.log(` Error: ${error.message}`);
@@ -35,18 +36,18 @@ async function main() {
3536

3637
// Example 2: Duplicate call detection
3738
console.log('2. Duplicate call detection:');
38-
const guardDup = new GardeFou({ on_violation_duplicate_call: 'warn' });
39+
const guardDup = GardeFou({ on_violation_duplicate_call: 'warn' });
3940

4041
// First call - should work
41-
const result1 = guardDup.call(expensiveApiCall, 'Hello world');
42+
const result1 = guardDup(expensiveApiCall, 'Hello world');
4243
console.log(` First call result: ${result1}`);
4344

4445
// Duplicate call - should warn
45-
const result2 = guardDup.call(expensiveApiCall, 'Hello world');
46+
const result2 = guardDup(expensiveApiCall, 'Hello world');
4647
console.log(` Duplicate call result: ${result2}`);
4748

4849
// Different call - should work
49-
const result3 = guardDup.call(expensiveApiCall, 'Different query');
50+
const result3 = guardDup(expensiveApiCall, 'Different query');
5051
console.log(` Different call result: ${result3}`);
5152

5253
console.log('\n' + '='.repeat(50) + '\n');
@@ -58,19 +59,19 @@ async function main() {
5859
on_violation_max_calls: 'raise',
5960
on_violation_duplicate_call: 'warn'
6061
});
61-
const guardProfile = new GardeFou({ profile });
62+
const guardProfile = GardeFou({ profile });
6263

6364
try {
6465
// First call
65-
const result = guardProfile.call(expensiveApiCall, 'Profile test 1');
66+
const result = guardProfile(expensiveApiCall, 'Profile test 1');
6667
console.log(` Call 1: ${result}`);
6768

6869
// Second call
69-
const result2 = guardProfile.call(expensiveApiCall, 'Profile test 2');
70+
const result2 = guardProfile(expensiveApiCall, 'Profile test 2');
7071
console.log(` Call 2: ${result2}`);
7172

7273
// Third call - should raise exception
73-
const result3 = guardProfile.call(expensiveApiCall, 'Profile test 3');
74+
const result3 = guardProfile(expensiveApiCall, 'Profile test 3');
7475
console.log(` Call 3: ${result3}`);
7576

7677
} catch (error) {
@@ -81,7 +82,7 @@ async function main() {
8182

8283
// Example 4: Async function protection
8384
console.log('4. Async function protection:');
84-
const guardAsync = new GardeFou({ max_calls: 2, on_violation_max_calls: 'raise' });
85+
const guardAsync = GardeFou({ max_calls: 2, on_violation_max_calls: 'raise' });
8586

8687
try {
8788
const asyncResult1 = await guardAsync.callAsync(expensiveAsyncApiCall, 'Async test 1');
@@ -108,15 +109,40 @@ async function main() {
108109
callbackTriggered = true;
109110
};
110111

111-
const guardCallback = new GardeFou({
112+
const guardCallback = GardeFou({
112113
max_calls: 1,
113114
on_violation_max_calls: customHandler
114115
});
115116

116-
guardCallback.call(expensiveApiCall, 'Callback test 1');
117-
guardCallback.call(expensiveApiCall, 'Callback test 2'); // Triggers callback
117+
guardCallback(expensiveApiCall, 'Callback test 1');
118+
guardCallback(expensiveApiCall, 'Callback test 2'); // Triggers callback
118119

119120
console.log(` Callback was triggered: ${callbackTriggered}`);
121+
122+
console.log('\n' + '='.repeat(50) + '\n');
123+
124+
// Example 6: Demonstrating both calling patterns work identically
125+
console.log('6. Both calling patterns (direct vs .call()):');
126+
const guardBoth = GardeFou({ max_calls: 4, on_violation_max_calls: 'warn' });
127+
128+
// Mix both calling patterns - they share the same quota
129+
console.log(` Starting call count: ${guardBoth.profile.call_count}`);
130+
131+
guardBoth(expensiveApiCall, 'Direct call 1');
132+
console.log(` After direct call 1: ${guardBoth.profile.call_count}`);
133+
134+
guardBoth.call(expensiveApiCall, 'Method call 1');
135+
console.log(` After method call 1: ${guardBoth.profile.call_count}`);
136+
137+
guardBoth(expensiveApiCall, 'Direct call 2');
138+
console.log(` After direct call 2: ${guardBoth.profile.call_count}`);
139+
140+
guardBoth.call(expensiveApiCall, 'Method call 2');
141+
console.log(` After method call 2: ${guardBoth.profile.call_count}`);
142+
143+
// This should trigger warning (5th call)
144+
guardBoth(expensiveApiCall, 'Direct call 3 - should warn');
145+
console.log(` After warning call: ${guardBoth.profile.call_count}`);
120146
}
121147

122148
if (require.main === module) {

0 commit comments

Comments
 (0)