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 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
+ [![Gem Version](https://badge.fury.io/rb/garde_fou.svg)](https://badge.fury.io/rb/garde_fou)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,3 @@
1
+ module Gardefou
2
+ VERSION = '0.1.0'
3
+ 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: []