garde_fou 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 +7 -0
- data/CHANGELOG.md +24 -0
- data/README.md +284 -0
- data/lib/gardefou/garde_fou.rb +52 -0
- data/lib/gardefou/profile.rb +96 -0
- data/lib/gardefou/storage.rb +58 -0
- data/lib/gardefou/version.rb +3 -0
- data/lib/gardefou/wrapper.rb +34 -0
- data/lib/gardefou.rb +18 -0
- metadata +113 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 878ca8aa7468ba775feda57eca93eab7d79a60a97a9a4efecf33e568b0c778e3
|
4
|
+
data.tar.gz: 3b36234d3c4abc60427ba78f754a57dae007c337fee7a72671fbbfc9eb647ca2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bf8468be03ec6c46314b56d6a7ca10b48af96e1964d2be80841f3629f244ac67390fc2ba96a9858a106d701f1162410ccbe1c6001b50cde012647b0b5c4c6bfd
|
7
|
+
data.tar.gz: f3bcf50270bd78ddd735e6e9b6750c2c1a1244a8d9b62f99a7d84799f40c5544f8b64d515630427b7f28508d240bde3408195089a25989dae14d41b57872e42a
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
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.0.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] - 2025-07-19
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- Initial Ruby implementation of garde-fou
|
14
|
+
- Call counting with configurable limits
|
15
|
+
- Duplicate call detection
|
16
|
+
- Multiple calling patterns (call, [], protect)
|
17
|
+
- Flexible violation handlers (warn, raise, custom procs)
|
18
|
+
- Configuration loading from JSON/YAML files
|
19
|
+
- GuardedClient mixin for class-level protection
|
20
|
+
- Comprehensive test suite with RSpec
|
21
|
+
- Ruby-idiomatic API design
|
22
|
+
|
23
|
+
[Unreleased]: https://github.com/rfievet/garde-fou/compare/v0.1.0...HEAD
|
24
|
+
[0.1.0]: https://github.com/rfievet/garde-fou/releases/tag/v0.1.0
|
data/README.md
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
# garde-fou (Ruby)
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/garde_fou)
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
5
|
+
|
6
|
+
**garde-fou** is a lightweight guard for protecting against accidental over-usage of paid API calls. It provides call counting and duplicate detection to help you avoid unexpected API bills.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- **Call counting** - Set maximum number of calls and get warnings or exceptions when exceeded
|
11
|
+
- **Duplicate detection** - Detect and handle repeated identical API calls
|
12
|
+
- **Flexible violation handling** - Choose to warn, raise exceptions, or use custom handlers
|
13
|
+
- **Configuration support** - Load settings from JSON/YAML files or set programmatically
|
14
|
+
- **Multiple calling patterns** - Ruby-idiomatic syntax with multiple ways to call
|
15
|
+
- **Mixin support** - Include GuardedClient module for class-level protection
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'garde_fou'
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
$ bundle install
|
28
|
+
|
29
|
+
Or install it yourself as:
|
30
|
+
|
31
|
+
$ gem install garde_fou
|
32
|
+
|
33
|
+
## Quick Start
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
require 'gardefou'
|
37
|
+
|
38
|
+
# Create a guard with call limits
|
39
|
+
guard = Gardefou::GardeFou.new(max_calls: 5, on_violation_max_calls: 'warn')
|
40
|
+
|
41
|
+
# Multiple calling patterns available:
|
42
|
+
# 1. Method call (explicit)
|
43
|
+
result = guard.call(your_api_method, "your", "arguments")
|
44
|
+
|
45
|
+
# 2. Bracket syntax (Ruby callable style)
|
46
|
+
result = guard[your_api_method, "your", "arguments"]
|
47
|
+
|
48
|
+
# 3. Protect method (semantic)
|
49
|
+
result = guard.protect(your_api_method, "your", "arguments")
|
50
|
+
```
|
51
|
+
|
52
|
+
## Usage Examples
|
53
|
+
|
54
|
+
### Basic Call Limiting
|
55
|
+
```ruby
|
56
|
+
require 'gardefou'
|
57
|
+
|
58
|
+
# Create a guard with a 3-call limit
|
59
|
+
guard = Gardefou::GardeFou.new(max_calls: 3, on_violation_max_calls: 'raise')
|
60
|
+
|
61
|
+
begin
|
62
|
+
# All calling patterns work identically
|
63
|
+
guard.call(api_method, 'query 1')
|
64
|
+
guard[api_method, 'query 2']
|
65
|
+
guard.protect(api_method, 'query 3')
|
66
|
+
guard.call(api_method, 'query 4') # This will raise!
|
67
|
+
rescue Gardefou::QuotaExceededError => e
|
68
|
+
puts "Call limit exceeded: #{e.message}"
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
72
|
+
### Duplicate Call Detection
|
73
|
+
```ruby
|
74
|
+
# Warn on duplicate calls
|
75
|
+
guard = Gardefou::GardeFou.new(on_violation_duplicate_call: 'warn')
|
76
|
+
|
77
|
+
guard.call(api_method, 'hello') # First call - OK
|
78
|
+
guard.call(api_method, 'hello') # Duplicate - Warning printed
|
79
|
+
guard.call(api_method, 'world') # Different call - OK
|
80
|
+
```
|
81
|
+
|
82
|
+
### Using Profiles
|
83
|
+
```ruby
|
84
|
+
# Create a profile with multiple rules
|
85
|
+
profile = Gardefou::Profile.new(
|
86
|
+
max_calls: 10,
|
87
|
+
on_violation_max_calls: 'raise',
|
88
|
+
on_violation_duplicate_call: 'warn'
|
89
|
+
)
|
90
|
+
|
91
|
+
guard = Gardefou::GardeFou.new(profile: profile)
|
92
|
+
```
|
93
|
+
|
94
|
+
### Configuration Files
|
95
|
+
```ruby
|
96
|
+
# Load from JSON/YAML file
|
97
|
+
profile = Gardefou::Profile.new(config: 'gardefou.config.json')
|
98
|
+
guard = Gardefou::GardeFou.new(profile: profile)
|
99
|
+
|
100
|
+
# Or pass config as hash
|
101
|
+
config = { 'max_calls' => 5, 'on_violation_max_calls' => 'warn' }
|
102
|
+
profile = Gardefou::Profile.new(config: config)
|
103
|
+
```
|
104
|
+
|
105
|
+
### Custom Violation Handlers
|
106
|
+
```ruby
|
107
|
+
custom_handler = proc do |profile|
|
108
|
+
puts "Custom violation! Call count: #{profile.call_count}"
|
109
|
+
# Send alert, log to service, etc.
|
110
|
+
end
|
111
|
+
|
112
|
+
guard = Gardefou::GardeFou.new(
|
113
|
+
max_calls: 5,
|
114
|
+
on_violation_max_calls: custom_handler
|
115
|
+
)
|
116
|
+
|
117
|
+
# All calling patterns work with custom handlers
|
118
|
+
guard.call(api_method, 'test') # Method call
|
119
|
+
guard[api_method, 'test'] # Bracket syntax
|
120
|
+
guard.protect(api_method, 'test') # Protect method
|
121
|
+
```
|
122
|
+
|
123
|
+
### Using the GuardedClient Mixin
|
124
|
+
```ruby
|
125
|
+
class APIClient
|
126
|
+
include Gardefou::GuardedClient
|
127
|
+
|
128
|
+
def expensive_call(query)
|
129
|
+
# Your expensive API call here
|
130
|
+
"Result for #{query}"
|
131
|
+
end
|
132
|
+
|
133
|
+
def another_call(data)
|
134
|
+
# Another API call
|
135
|
+
"Processed #{data}"
|
136
|
+
end
|
137
|
+
|
138
|
+
# Guard specific methods
|
139
|
+
guard_method :expensive_call, max_calls: 10, on_violation_max_calls: 'warn'
|
140
|
+
|
141
|
+
# Guard all methods matching a pattern
|
142
|
+
guard_methods /call$/, max_calls: 5, on_violation_duplicate_call: 'warn'
|
143
|
+
end
|
144
|
+
|
145
|
+
client = APIClient.new
|
146
|
+
client.expensive_call('test') # Protected automatically
|
147
|
+
```
|
148
|
+
|
149
|
+
## Real-World Examples
|
150
|
+
|
151
|
+
### OpenAI API Protection
|
152
|
+
```ruby
|
153
|
+
require 'gardefou'
|
154
|
+
|
155
|
+
# Assuming you have an OpenAI client
|
156
|
+
guard = Gardefou::GardeFou.new(
|
157
|
+
max_calls: 100,
|
158
|
+
on_violation_max_calls: 'warn',
|
159
|
+
on_violation_duplicate_call: 'warn'
|
160
|
+
)
|
161
|
+
|
162
|
+
# Before: Direct API call
|
163
|
+
# response = openai_client.completions(prompt: 'Hello!')
|
164
|
+
|
165
|
+
# After: Protected API call (choose your preferred syntax)
|
166
|
+
response = guard.call(openai_client.method(:completions), prompt: 'Hello!')
|
167
|
+
# or
|
168
|
+
response = guard[openai_client.method(:completions), prompt: 'Hello!']
|
169
|
+
# or
|
170
|
+
response = guard.protect(openai_client.method(:completions), prompt: 'Hello!')
|
171
|
+
```
|
172
|
+
|
173
|
+
### Multiple API Services
|
174
|
+
```ruby
|
175
|
+
guard = Gardefou::GardeFou.new(max_calls: 50)
|
176
|
+
|
177
|
+
# Protect different APIs with the same guard
|
178
|
+
openai_result = guard.call(openai_client.method(:completions), prompt: 'test')
|
179
|
+
anthropic_result = guard[anthropic_client.method(:messages), message: 'test']
|
180
|
+
cohere_result = guard.protect(cohere_client.method(:generate), text: 'test')
|
181
|
+
|
182
|
+
puts "Total API calls made: #{guard.profile.call_count}"
|
183
|
+
```
|
184
|
+
|
185
|
+
## Configuration Options
|
186
|
+
|
187
|
+
- `max_calls`: Maximum number of calls allowed (-1 for unlimited)
|
188
|
+
- `on_violation_max_calls`: Handler when call limit exceeded (`'warn'`, `'raise'`, or Proc)
|
189
|
+
- `on_violation_duplicate_call`: Handler for duplicate calls (`'warn'`, `'raise'`, or Proc)
|
190
|
+
- `on_violation`: Default handler for all violations
|
191
|
+
|
192
|
+
## API Reference
|
193
|
+
|
194
|
+
### GardeFou Class
|
195
|
+
|
196
|
+
#### Constructor
|
197
|
+
```ruby
|
198
|
+
Gardefou::GardeFou.new(
|
199
|
+
profile: nil,
|
200
|
+
max_calls: nil,
|
201
|
+
on_violation: nil,
|
202
|
+
on_violation_max_calls: nil,
|
203
|
+
on_violation_duplicate_call: nil
|
204
|
+
)
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Methods
|
208
|
+
- `call(method, *args, **kwargs, &block)` - Execute a method with protection
|
209
|
+
- `[method, *args, **kwargs, &block]` - Ruby callable syntax (alias for call)
|
210
|
+
- `protect(method, *args, **kwargs, &block)` - Semantic alias for call
|
211
|
+
|
212
|
+
### Profile Class
|
213
|
+
|
214
|
+
#### Constructor
|
215
|
+
```ruby
|
216
|
+
Gardefou::Profile.new(
|
217
|
+
config: nil,
|
218
|
+
max_calls: nil,
|
219
|
+
on_violation: nil,
|
220
|
+
on_violation_max_calls: nil,
|
221
|
+
on_violation_duplicate_call: nil
|
222
|
+
)
|
223
|
+
```
|
224
|
+
|
225
|
+
### GuardedClient Module
|
226
|
+
|
227
|
+
#### Class Methods
|
228
|
+
- `guard_method(method_name, **options)` - Guard a specific method
|
229
|
+
- `guard_methods(pattern, **options)` - Guard methods matching a pattern
|
230
|
+
|
231
|
+
#### Instance Methods
|
232
|
+
- `create_guard(**options)` - Create an instance-level guard
|
233
|
+
|
234
|
+
## How It Works
|
235
|
+
|
236
|
+
garde-fou works by wrapping your method calls. Instead of calling your API method directly, you call it through the guard:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# Before
|
240
|
+
result = openai_client.completions(prompt: 'Hello!')
|
241
|
+
|
242
|
+
# After - choose your preferred syntax:
|
243
|
+
guard = Gardefou::GardeFou.new(max_calls: 10)
|
244
|
+
|
245
|
+
# Option 1: Method call
|
246
|
+
result = guard.call(openai_client.method(:completions), prompt: 'Hello!')
|
247
|
+
|
248
|
+
# Option 2: Bracket syntax (Ruby callable style)
|
249
|
+
result = guard[openai_client.method(:completions), prompt: 'Hello!']
|
250
|
+
|
251
|
+
# Option 3: Protect method (semantic)
|
252
|
+
result = guard.protect(openai_client.method(:completions), prompt: 'Hello!')
|
253
|
+
```
|
254
|
+
|
255
|
+
The guard tracks calls and enforces your configured rules before executing the actual method.
|
256
|
+
|
257
|
+
## Development
|
258
|
+
|
259
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
260
|
+
|
261
|
+
```bash
|
262
|
+
# Install dependencies
|
263
|
+
bundle install
|
264
|
+
|
265
|
+
# Run tests
|
266
|
+
rake spec
|
267
|
+
|
268
|
+
# Run example
|
269
|
+
rake example
|
270
|
+
|
271
|
+
# Run RuboCop
|
272
|
+
rake rubocop
|
273
|
+
|
274
|
+
# Run all checks
|
275
|
+
rake check
|
276
|
+
```
|
277
|
+
|
278
|
+
## Contributing
|
279
|
+
|
280
|
+
This is part of the multi-language garde-fou toolkit. See the main repository for contributing guidelines.
|
281
|
+
|
282
|
+
## License
|
283
|
+
|
284
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require_relative 'profile'
|
2
|
+
|
3
|
+
module Gardefou
|
4
|
+
class GardeFou
|
5
|
+
attr_reader :profile
|
6
|
+
|
7
|
+
def initialize(profile: nil, **profile_options)
|
8
|
+
@profile = profile || Profile.new(**profile_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Main callable interface - allows guard.call(method, *args, **kwargs)
|
12
|
+
def call(method, *args, **kwargs, &block)
|
13
|
+
# Extract method name for tracking
|
14
|
+
method_name = extract_method_name(method)
|
15
|
+
|
16
|
+
# Run profile checks
|
17
|
+
@profile.check(method_name, args, kwargs)
|
18
|
+
|
19
|
+
# Execute the method
|
20
|
+
if kwargs.empty?
|
21
|
+
# Ruby 2.6 compatibility - avoid passing empty kwargs
|
22
|
+
method.call(*args, &block)
|
23
|
+
else
|
24
|
+
method.call(*args, **kwargs, &block)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Ruby-style callable interface - allows guard.(method, *args, **kwargs)
|
29
|
+
# This is Ruby's equivalent to Python's __call__
|
30
|
+
alias [] call
|
31
|
+
|
32
|
+
# Alternative syntax for those who prefer it
|
33
|
+
def protect(method, *args, **kwargs, &block)
|
34
|
+
call(method, *args, **kwargs, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def extract_method_name(method)
|
40
|
+
case method
|
41
|
+
when Method
|
42
|
+
"#{method.receiver.class}##{method.name}"
|
43
|
+
when UnboundMethod
|
44
|
+
"#{method.owner}##{method.name}"
|
45
|
+
when Proc
|
46
|
+
method.source_location ? "Proc@#{method.source_location.join(':')}" : 'Proc'
|
47
|
+
else
|
48
|
+
method.class.name
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Gardefou
|
5
|
+
class QuotaExceededError < StandardError; end
|
6
|
+
|
7
|
+
class Profile
|
8
|
+
attr_reader :max_calls, :on_violation, :on_violation_max_calls, :on_violation_duplicate_call, :call_count
|
9
|
+
|
10
|
+
def initialize(config: nil, max_calls: nil, on_violation: nil,
|
11
|
+
on_violation_max_calls: nil, on_violation_duplicate_call: nil)
|
12
|
+
# Load base data from file or hash
|
13
|
+
data = {}
|
14
|
+
|
15
|
+
if config.is_a?(String)
|
16
|
+
# Load from file
|
17
|
+
content = File.read(config)
|
18
|
+
data = if config.end_with?('.yaml', '.yml')
|
19
|
+
YAML.safe_load(content)
|
20
|
+
else
|
21
|
+
JSON.parse(content)
|
22
|
+
end
|
23
|
+
elsif config.is_a?(Hash)
|
24
|
+
data = config.dup
|
25
|
+
end
|
26
|
+
|
27
|
+
# Override with explicit options
|
28
|
+
data['max_calls'] = max_calls unless max_calls.nil?
|
29
|
+
data['on_violation'] = on_violation unless on_violation.nil?
|
30
|
+
data['on_violation_max_calls'] = on_violation_max_calls unless on_violation_max_calls.nil?
|
31
|
+
data['on_violation_duplicate_call'] = on_violation_duplicate_call unless on_violation_duplicate_call.nil?
|
32
|
+
|
33
|
+
# Assign settings with defaults
|
34
|
+
@max_calls = data['max_calls'] || -1
|
35
|
+
@on_violation = data['on_violation'] || 'raise'
|
36
|
+
@on_violation_max_calls = data['on_violation_max_calls'] || @on_violation
|
37
|
+
@on_violation_duplicate_call = data['on_violation_duplicate_call'] || @on_violation
|
38
|
+
|
39
|
+
@call_count = 0
|
40
|
+
@call_signatures = Set.new
|
41
|
+
|
42
|
+
# Track which rules were explicitly configured
|
43
|
+
@max_calls_enabled = data.key?('max_calls') && @max_calls >= 0
|
44
|
+
@dup_enabled = data.key?('on_violation_duplicate_call')
|
45
|
+
end
|
46
|
+
|
47
|
+
def check(fn_name = nil, args = [], kwargs = {})
|
48
|
+
check_max_call if @max_calls_enabled
|
49
|
+
check_duplicate(fn_name, args, kwargs) if @dup_enabled
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def check_max_call
|
55
|
+
@call_count += 1
|
56
|
+
return unless @call_count > @max_calls
|
57
|
+
|
58
|
+
msg = "GardeFou: call quota exceeded (#{@call_count}/#{@max_calls})"
|
59
|
+
handle_violation(@on_violation_max_calls, msg)
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_duplicate(fn_name = nil, args = [], kwargs = {})
|
63
|
+
signature = create_signature(fn_name, args, kwargs)
|
64
|
+
|
65
|
+
if @call_signatures.include?(signature)
|
66
|
+
msg = "GardeFou: duplicate call detected for #{fn_name} with args #{args.inspect} and kwargs #{kwargs.inspect}"
|
67
|
+
handle_violation(@on_violation_duplicate_call, msg)
|
68
|
+
else
|
69
|
+
@call_signatures.add(signature)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_signature(fn_name, args, kwargs)
|
74
|
+
# Create a deterministic signature for duplicate detection
|
75
|
+
sorted_kwargs = kwargs.sort.to_h
|
76
|
+
{
|
77
|
+
fn_name: fn_name,
|
78
|
+
args: args,
|
79
|
+
kwargs: sorted_kwargs
|
80
|
+
}.to_json
|
81
|
+
end
|
82
|
+
|
83
|
+
def handle_violation(handler, message)
|
84
|
+
case handler
|
85
|
+
when 'warn'
|
86
|
+
warn(message)
|
87
|
+
when 'raise'
|
88
|
+
raise QuotaExceededError, message
|
89
|
+
when Proc
|
90
|
+
handler.call(self)
|
91
|
+
else
|
92
|
+
raise ArgumentError, "Invalid violation handler: #{handler}"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Gardefou
|
5
|
+
# Utility class for creating call signatures for duplicate detection
|
6
|
+
class CallSignature
|
7
|
+
attr_reader :fn_name, :args, :kwargs
|
8
|
+
|
9
|
+
def initialize(fn_name, args, kwargs)
|
10
|
+
@fn_name = fn_name
|
11
|
+
@args = args
|
12
|
+
@kwargs = kwargs
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
# Create deterministic string representation
|
17
|
+
sorted_kwargs = @kwargs.sort.to_h
|
18
|
+
{
|
19
|
+
fn_name: @fn_name,
|
20
|
+
args: @args,
|
21
|
+
kwargs: sorted_kwargs
|
22
|
+
}.to_json
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
other.is_a?(CallSignature) && to_s == other.to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def hash
|
30
|
+
to_s.hash
|
31
|
+
end
|
32
|
+
|
33
|
+
alias eql? ==
|
34
|
+
end
|
35
|
+
|
36
|
+
# Storage adapter for tracking calls (future extension point)
|
37
|
+
class StorageAdapter
|
38
|
+
def initialize
|
39
|
+
@signatures = Set.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def store_signature(signature)
|
43
|
+
@signatures.add(signature)
|
44
|
+
end
|
45
|
+
|
46
|
+
def signature_exists?(signature)
|
47
|
+
@signatures.include?(signature)
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear
|
51
|
+
@signatures.clear
|
52
|
+
end
|
53
|
+
|
54
|
+
def count
|
55
|
+
@signatures.size
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative 'garde_fou'
|
2
|
+
|
3
|
+
module Gardefou
|
4
|
+
# Mixin module to add garde-fou protection to any class
|
5
|
+
module GuardedClient
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Class-level method to set up protection for specific methods
|
12
|
+
def guard_method(method_name, **options)
|
13
|
+
original_method = instance_method(method_name)
|
14
|
+
guard = GardeFou.new(**options)
|
15
|
+
|
16
|
+
define_method(method_name) do |*args, **kwargs, &block|
|
17
|
+
guard.call(original_method.bind(self), *args, **kwargs, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Protect all methods matching a pattern
|
22
|
+
def guard_methods(pattern, **options)
|
23
|
+
instance_methods.grep(pattern).each do |method_name|
|
24
|
+
guard_method(method_name, **options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Instance-level guard creation
|
30
|
+
def create_guard(**options)
|
31
|
+
GardeFou.new(**options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/gardefou.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Main garde-fou library
|
2
|
+
require_relative 'gardefou/version'
|
3
|
+
require_relative 'gardefou/profile'
|
4
|
+
require_relative 'gardefou/garde_fou'
|
5
|
+
require_relative 'gardefou/storage'
|
6
|
+
require_relative 'gardefou/wrapper'
|
7
|
+
|
8
|
+
module Gardefou
|
9
|
+
# Convenience method to create a new guard
|
10
|
+
def self.new(**options)
|
11
|
+
GardeFou.new(**options)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Create a guard with a profile
|
15
|
+
def self.with_profile(profile)
|
16
|
+
GardeFou.new(profile: profile)
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: garde_fou
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robin Fiévet
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-08-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '13.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '13.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
description: A lightweight guard for protecting against accidental over-usage of paid
|
70
|
+
API calls. Provides call counting and duplicate detection to help you avoid unexpected
|
71
|
+
API bills.
|
72
|
+
email:
|
73
|
+
- robinfievet@gmail.com
|
74
|
+
executables: []
|
75
|
+
extensions: []
|
76
|
+
extra_rdoc_files: []
|
77
|
+
files:
|
78
|
+
- CHANGELOG.md
|
79
|
+
- README.md
|
80
|
+
- lib/gardefou.rb
|
81
|
+
- lib/gardefou/garde_fou.rb
|
82
|
+
- lib/gardefou/profile.rb
|
83
|
+
- lib/gardefou/storage.rb
|
84
|
+
- lib/gardefou/version.rb
|
85
|
+
- lib/gardefou/wrapper.rb
|
86
|
+
homepage: https://github.com/rfievet/garde-fou
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata:
|
90
|
+
homepage_uri: https://github.com/rfievet/garde-fou
|
91
|
+
source_code_uri: https://github.com/rfievet/garde-fou
|
92
|
+
changelog_uri: https://github.com/rfievet/garde-fou/blob/main/ruby/CHANGELOG.md
|
93
|
+
rubygems_mfa_required: 'true'
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 2.6.0
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubygems_version: 3.0.3.1
|
110
|
+
signing_key:
|
111
|
+
specification_version: 4
|
112
|
+
summary: Protective wrappers around paid API clients with quotas & duplicate detection
|
113
|
+
test_files: []
|