stellwerk-ruby 0.1.0 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +234 -28
- data/lib/stellwerk/api_client.rb +152 -0
- data/lib/stellwerk/configuration.rb +80 -0
- data/lib/stellwerk/errors.rb +17 -0
- data/lib/stellwerk/flow.rb +71 -6
- data/lib/stellwerk/license.rb +201 -0
- data/lib/stellwerk/metering.rb +168 -0
- data/lib/stellwerk/version.rb +1 -1
- data/lib/stellwerk.rb +72 -9
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53518cb9a1c9f94bd19252dcecf8fce62e4cc808e926ee51bf05dd5aa93a3826
|
|
4
|
+
data.tar.gz: '0797c9b22de3cc4fdd6cdea65b117abd90437b9c80703577060e2765ccf0d2ca'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 169634b2e34156ab8f7742611fdf8739973b94be79c6458aa5b7bda06b7e8c42ec54d358e9e0fd44b683b6b6e3e34eff51bf0b23c1b1907dde72feebf1af9663
|
|
7
|
+
data.tar.gz: a196cf2f9ec2f44238af3d90d68838d5d0fdc36b4df47b5895b97100dc86f77d1e8b2ec9d029016ce09dcebfe746ce1be3a7ee198e09983bfb6f6f73a0d26968
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.1] - 2026-02-02
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Updated default API URL to `https://stellwerk.io`
|
|
13
|
+
- Updated license validation endpoint to `POST /api/v1/sdk/validate`
|
|
14
|
+
- Updated API response parsing to use `limits.monthly_executions` and `limits.current_count`
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `verify_ssl` configuration option to disable SSL verification for development
|
|
19
|
+
|
|
20
|
+
## [0.2.0] - 2026-02-01
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **License validation**: API key required for all flow executions
|
|
25
|
+
- Lazy validation on first execution with result caching
|
|
26
|
+
- Configurable cache TTL from server response
|
|
27
|
+
- **Execution metering**: Track and report flow executions
|
|
28
|
+
- Batch reporting at configurable intervals (default: 60 seconds)
|
|
29
|
+
- Automatic sync with Stellwerk API
|
|
30
|
+
- **Offline grace period**: Continue operating when server is unreachable
|
|
31
|
+
- Default: 24 hours or 100 executions
|
|
32
|
+
- Configurable via `offline_grace_period` and `offline_grace_executions`
|
|
33
|
+
- **New configuration options**:
|
|
34
|
+
- `api_key` - Required API key for license validation
|
|
35
|
+
- `api_url` - API endpoint (default: https://stellwerk.io)
|
|
36
|
+
- `sync_interval` - Batch reporting interval in seconds
|
|
37
|
+
- `offline_grace_period` - Offline operation allowance in seconds
|
|
38
|
+
- `offline_grace_executions` - Max offline executions
|
|
39
|
+
- **New error classes**:
|
|
40
|
+
- `LicenseError` - Raised when API key is missing or invalid
|
|
41
|
+
- `QuotaExceededError` - Raised when execution limit is reached
|
|
42
|
+
- `ApiError` - Raised on server communication failures
|
|
43
|
+
- **New helper methods**:
|
|
44
|
+
- `Stellwerk.licensed?` - Check if SDK is properly licensed
|
|
45
|
+
- `Stellwerk.license` - Access license details (plan, limits, usage)
|
|
46
|
+
- `Stellwerk.metering` - Access metering system
|
|
47
|
+
- Thread-safe license and metering implementations using Mutex
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- `Stellwerk.configure` now yields a `Configuration` object instead of the module
|
|
52
|
+
- `Flow#execute` now enforces license validation and tracks executions
|
|
53
|
+
- Improved documentation with comprehensive troubleshooting guide
|
|
54
|
+
|
|
8
55
|
## [0.1.0] - 2026-01-31
|
|
9
56
|
|
|
10
57
|
### Added
|
data/README.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A standalone Ruby gem for evaluating pre-compiled Stellwerk flows without any Rails or ActiveRecord dependencies.
|
|
4
4
|
|
|
5
|
+
## What is Stellwerk?
|
|
6
|
+
|
|
7
|
+
[Stellwerk](https://stellwerk.io) is a visual platform for building decision flows and business logic without code. Design complex calculations, conditional branching, and data transformations using an intuitive drag-and-drop interface, then export your flows as compiled JSON to run anywhere.
|
|
8
|
+
|
|
9
|
+
This gem lets you execute those compiled flows in any Ruby application.
|
|
10
|
+
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
7
13
|
Add this line to your application's Gemfile:
|
|
@@ -22,55 +28,209 @@ Or install it yourself as:
|
|
|
22
28
|
gem install stellwerk-ruby
|
|
23
29
|
```
|
|
24
30
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
### Simple Usage
|
|
31
|
+
## Quick Start
|
|
28
32
|
|
|
29
33
|
```ruby
|
|
30
34
|
require 'stellwerk'
|
|
31
35
|
|
|
32
|
-
#
|
|
33
|
-
|
|
36
|
+
# Configure your API key (required)
|
|
37
|
+
Stellwerk.configure do |config|
|
|
38
|
+
config.api_key = ENV['STELLWERK_API_KEY']
|
|
39
|
+
end
|
|
34
40
|
|
|
35
|
-
#
|
|
41
|
+
# Load and execute a flow
|
|
42
|
+
flow = Stellwerk::Flow.load('pricing.compiled.json')
|
|
36
43
|
result = flow.execute(quantity: 10, price: 25.00)
|
|
37
44
|
|
|
38
45
|
if result.success?
|
|
39
|
-
puts result.outputs[:total]
|
|
46
|
+
puts "Total: #{result.outputs[:total]}"
|
|
47
|
+
else
|
|
48
|
+
puts "Errors: #{result.errors}"
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
### Required: API Key
|
|
55
|
+
|
|
56
|
+
All flow executions require a valid API key. Get your key from [stellwerk.io](https://stellwerk.io).
|
|
57
|
+
|
|
58
|
+
Stellwerk provides two types of API keys:
|
|
59
|
+
|
|
60
|
+
| Key Type | Prefix | Purpose | Quota |
|
|
61
|
+
|----------|--------|---------|-------|
|
|
62
|
+
| **Test** | `sk_test_...` | Development and testing | Not counted |
|
|
63
|
+
| **Live** | `sk_live_...` | Production use | Counted against plan |
|
|
64
|
+
|
|
65
|
+
Use test keys during development to avoid consuming your quota. Switch to live keys when deploying to production.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Stellwerk.configure do |config|
|
|
69
|
+
# Use test key in development, live key in production
|
|
70
|
+
config.api_key = ENV['STELLWERK_API_KEY']
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Or use the shorthand
|
|
74
|
+
Stellwerk.api_key = ENV['STELLWERK_API_KEY']
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Tip**: Set different environment variables per environment:
|
|
78
|
+
```bash
|
|
79
|
+
# .env.development
|
|
80
|
+
STELLWERK_API_KEY=sk_test_your_test_key
|
|
81
|
+
|
|
82
|
+
# .env.production
|
|
83
|
+
STELLWERK_API_KEY=sk_live_your_live_key
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Full Configuration Options
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
Stellwerk.configure do |config|
|
|
90
|
+
# Required: Your API key from stellwerk.io
|
|
91
|
+
config.api_key = ENV['STELLWERK_API_KEY']
|
|
92
|
+
|
|
93
|
+
# Optional: API endpoint (default: https://stellwerk.io)
|
|
94
|
+
config.api_url = 'https://stellwerk.io'
|
|
95
|
+
|
|
96
|
+
# Optional: Interval for batch reporting executions (default: 60 seconds)
|
|
97
|
+
config.sync_interval = 60
|
|
98
|
+
|
|
99
|
+
# Optional: How long to allow offline operation (default: 86400 = 24 hours)
|
|
100
|
+
config.offline_grace_period = 86_400
|
|
101
|
+
|
|
102
|
+
# Optional: Max executions during offline grace period (default: 100)
|
|
103
|
+
config.offline_grace_executions = 100
|
|
104
|
+
|
|
105
|
+
# Optional: Logger for debugging
|
|
106
|
+
config.logger = Logger.new(STDOUT)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Usage Examples
|
|
111
|
+
|
|
112
|
+
### Basic Execution
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
flow = Stellwerk::Flow.load('calculator.compiled.json')
|
|
116
|
+
result = flow.execute(x: 10, y: 20)
|
|
117
|
+
|
|
118
|
+
puts result.outputs[:sum] # => 30
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Error Handling
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
flow = Stellwerk::Flow.load('pricing.compiled.json')
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
result = flow.execute(params)
|
|
128
|
+
|
|
129
|
+
if result.success?
|
|
130
|
+
process(result.outputs)
|
|
131
|
+
else
|
|
132
|
+
# Flow executed but returned errors (validation, business logic)
|
|
133
|
+
log_errors(result.errors)
|
|
134
|
+
end
|
|
135
|
+
rescue Stellwerk::LicenseError => e
|
|
136
|
+
# API key missing or invalid
|
|
137
|
+
puts "License error: #{e.message}"
|
|
138
|
+
rescue Stellwerk::QuotaExceededError => e
|
|
139
|
+
# Execution limit reached
|
|
140
|
+
puts "Quota exceeded: #{e.message}"
|
|
141
|
+
rescue Stellwerk::ApiError => e
|
|
142
|
+
# Server communication error
|
|
143
|
+
puts "API error: #{e.message} (status: #{e.status_code})"
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Batch Processing
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
flow = Stellwerk::Flow.load('invoice.compiled.json')
|
|
151
|
+
|
|
152
|
+
invoices.each do |invoice|
|
|
153
|
+
result = flow.execute(invoice.to_h)
|
|
154
|
+
|
|
155
|
+
if result.success?
|
|
156
|
+
invoice.update!(
|
|
157
|
+
subtotal: result.outputs[:subtotal],
|
|
158
|
+
tax: result.outputs[:tax],
|
|
159
|
+
total: result.outputs[:total]
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Executions are batched and reported automatically
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Checking License Status
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# Check if properly licensed before processing
|
|
171
|
+
if Stellwerk.licensed?
|
|
172
|
+
process_flows
|
|
40
173
|
else
|
|
41
|
-
puts
|
|
174
|
+
puts "Please configure a valid API key"
|
|
42
175
|
end
|
|
176
|
+
|
|
177
|
+
# Access license details
|
|
178
|
+
license = Stellwerk.license
|
|
179
|
+
puts "Plan: #{license.plan}"
|
|
180
|
+
puts "Limit: #{license.execution_limit}"
|
|
181
|
+
puts "Used: #{license.executions_used}"
|
|
43
182
|
```
|
|
44
183
|
|
|
45
184
|
### Direct Evaluator Usage
|
|
46
185
|
|
|
47
|
-
For
|
|
186
|
+
For advanced use cases, you can use the evaluator directly:
|
|
48
187
|
|
|
49
188
|
```ruby
|
|
50
|
-
require 'stellwerk'
|
|
51
189
|
require 'json'
|
|
52
190
|
|
|
53
191
|
json = JSON.parse(File.read('flow.json'))
|
|
54
192
|
result = Stellwerk::Evaluator.call(
|
|
55
193
|
compiled_json: json,
|
|
56
|
-
params: { x: 10, y: 20 }
|
|
194
|
+
params: { x: 10, y: 20 },
|
|
195
|
+
metadata: { source: 'api', user_id: 123 }
|
|
57
196
|
)
|
|
58
197
|
|
|
59
|
-
puts result.outputs
|
|
60
|
-
puts result.context
|
|
61
|
-
puts result.applied_nodes
|
|
198
|
+
puts result.outputs # Output values
|
|
199
|
+
puts result.context # Full execution context
|
|
200
|
+
puts result.applied_nodes # Executed node IDs
|
|
62
201
|
```
|
|
63
202
|
|
|
64
|
-
|
|
203
|
+
## Offline Mode
|
|
65
204
|
|
|
66
|
-
|
|
67
|
-
require 'logger'
|
|
205
|
+
If the Stellwerk server becomes unreachable after a successful license validation, the SDK enters an offline grace period:
|
|
68
206
|
|
|
69
|
-
|
|
70
|
-
|
|
207
|
+
- **Default duration**: 24 hours
|
|
208
|
+
- **Default execution limit**: 100 executions
|
|
209
|
+
|
|
210
|
+
During offline mode, executions are tracked locally and reported when connectivity is restored.
|
|
211
|
+
|
|
212
|
+
```ruby
|
|
213
|
+
# Check if operating in offline mode
|
|
214
|
+
if Stellwerk.license.offline?
|
|
215
|
+
puts "Operating in offline mode"
|
|
71
216
|
end
|
|
72
217
|
```
|
|
73
218
|
|
|
219
|
+
## Result Object
|
|
220
|
+
|
|
221
|
+
Execution returns a `Stellwerk::Result` object:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
result = flow.execute(params)
|
|
225
|
+
|
|
226
|
+
result.success? # => true/false
|
|
227
|
+
result.failure? # => true/false
|
|
228
|
+
result.outputs # => Hash of output values
|
|
229
|
+
result.errors # => Array of error messages/hashes
|
|
230
|
+
result.applied_nodes # => Array of executed node IDs
|
|
231
|
+
result.context # => Full execution context
|
|
232
|
+
```
|
|
233
|
+
|
|
74
234
|
## Compiled JSON Format
|
|
75
235
|
|
|
76
236
|
The gem expects compiled flow JSON in this structure:
|
|
@@ -135,21 +295,67 @@ The gem includes collection functions for use in formulas:
|
|
|
135
295
|
- `SUMIF(array, "predicate", "projection")` - Conditional sum
|
|
136
296
|
- `COUNTIF(array, "predicate")` - Conditional count
|
|
137
297
|
|
|
138
|
-
##
|
|
298
|
+
## Troubleshooting
|
|
139
299
|
|
|
140
|
-
|
|
300
|
+
### "API key is required"
|
|
301
|
+
|
|
302
|
+
You must configure an API key before executing flows:
|
|
141
303
|
|
|
142
304
|
```ruby
|
|
143
|
-
|
|
305
|
+
Stellwerk.configure do |config|
|
|
306
|
+
config.api_key = ENV['STELLWERK_API_KEY']
|
|
307
|
+
end
|
|
308
|
+
```
|
|
144
309
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
310
|
+
Get your API key from [stellwerk.io](https://stellwerk.io).
|
|
311
|
+
|
|
312
|
+
### "Invalid API key"
|
|
313
|
+
|
|
314
|
+
Your API key is not recognized. Verify:
|
|
315
|
+
- The key is correctly copied (no extra spaces)
|
|
316
|
+
- The key is active in your Stellwerk dashboard
|
|
317
|
+
- You're using the correct key type (`sk_test_...` for development, `sk_live_...` for production)
|
|
318
|
+
|
|
319
|
+
### "Execution limit reached"
|
|
320
|
+
|
|
321
|
+
You've exceeded your plan's monthly execution quota. Options:
|
|
322
|
+
- Wait for the next billing cycle
|
|
323
|
+
- Upgrade your plan at [stellwerk.io](https://stellwerk.io)
|
|
324
|
+
|
|
325
|
+
### "Offline execution limit reached"
|
|
326
|
+
|
|
327
|
+
During offline mode, you've exceeded the allowed offline executions. Restore network connectivity to continue.
|
|
328
|
+
|
|
329
|
+
### Debug Mode
|
|
330
|
+
|
|
331
|
+
Enable logging to see detailed execution information:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
Stellwerk.configure do |config|
|
|
335
|
+
config.api_key = ENV['STELLWERK_API_KEY']
|
|
336
|
+
config.logger = Logger.new(STDOUT)
|
|
337
|
+
config.logger.level = Logger::DEBUG
|
|
338
|
+
end
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Flow Validation Errors
|
|
342
|
+
|
|
343
|
+
If `result.failure?` returns true, check `result.errors` for details:
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
result = flow.execute(params)
|
|
347
|
+
if result.failure?
|
|
348
|
+
result.errors.each do |error|
|
|
349
|
+
puts "Error: #{error}"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
151
352
|
```
|
|
152
353
|
|
|
354
|
+
Common causes:
|
|
355
|
+
- Missing required input parameters
|
|
356
|
+
- Invalid data types
|
|
357
|
+
- Formula evaluation errors
|
|
358
|
+
|
|
153
359
|
## Development
|
|
154
360
|
|
|
155
361
|
After checking out the repo, run:
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "openssl"
|
|
7
|
+
|
|
8
|
+
module Stellwerk
|
|
9
|
+
# HTTP client wrapper for Stellwerk API communication
|
|
10
|
+
#
|
|
11
|
+
# Uses net/http from Ruby stdlib for zero external dependencies.
|
|
12
|
+
# Handles authentication, retries, timeouts, and JSON parsing.
|
|
13
|
+
#
|
|
14
|
+
class ApiClient
|
|
15
|
+
# Default timeout for HTTP requests (seconds)
|
|
16
|
+
DEFAULT_TIMEOUT = 10
|
|
17
|
+
|
|
18
|
+
# Maximum total attempts for transient failures (initial + retries)
|
|
19
|
+
MAX_ATTEMPTS = 3
|
|
20
|
+
|
|
21
|
+
# Delay between retries (seconds)
|
|
22
|
+
RETRY_DELAY = 1
|
|
23
|
+
|
|
24
|
+
# @param api_url [String] Base URL for the API
|
|
25
|
+
# @param api_key [String] API key for authentication
|
|
26
|
+
# @param timeout [Integer] Request timeout in seconds
|
|
27
|
+
# @param verify_ssl [Boolean] Whether to verify SSL certificates
|
|
28
|
+
def initialize(api_url:, api_key:, timeout: DEFAULT_TIMEOUT, verify_ssl: true)
|
|
29
|
+
@api_url = api_url.chomp("/")
|
|
30
|
+
@api_key = api_key
|
|
31
|
+
@timeout = timeout
|
|
32
|
+
@verify_ssl = verify_ssl
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Make a GET request
|
|
36
|
+
#
|
|
37
|
+
# @param path [String] API endpoint path
|
|
38
|
+
# @return [Hash] Parsed JSON response
|
|
39
|
+
# @raise [ApiError] On request failure
|
|
40
|
+
def get(path)
|
|
41
|
+
request(:get, path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Make a POST request
|
|
45
|
+
#
|
|
46
|
+
# @param path [String] API endpoint path
|
|
47
|
+
# @param body [Hash] Request body (will be JSON encoded)
|
|
48
|
+
# @return [Hash] Parsed JSON response
|
|
49
|
+
# @raise [ApiError] On request failure
|
|
50
|
+
def post(path, body = {})
|
|
51
|
+
request(:post, path, body)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def request(method, path, body = nil)
|
|
57
|
+
uri = URI.parse("#{@api_url}#{path}")
|
|
58
|
+
retries = 0
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
response = execute_request(method, uri, body)
|
|
62
|
+
handle_response(response)
|
|
63
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ENETUNREACH,
|
|
64
|
+
Net::OpenTimeout, Net::ReadTimeout, SocketError => e
|
|
65
|
+
retries += 1
|
|
66
|
+
if retries < MAX_ATTEMPTS
|
|
67
|
+
sleep(RETRY_DELAY * retries)
|
|
68
|
+
retry
|
|
69
|
+
end
|
|
70
|
+
raise ApiError.new(
|
|
71
|
+
"Failed to connect to Stellwerk API: #{e.message}",
|
|
72
|
+
status_code: nil,
|
|
73
|
+
response_body: nil
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def execute_request(method, uri, body)
|
|
79
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
80
|
+
http.use_ssl = uri.scheme == "https"
|
|
81
|
+
http.verify_mode = @verify_ssl ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
82
|
+
http.open_timeout = @timeout
|
|
83
|
+
http.read_timeout = @timeout
|
|
84
|
+
|
|
85
|
+
request = build_request(method, uri, body)
|
|
86
|
+
http.request(request)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_request(method, uri, body)
|
|
90
|
+
request_class = method == :get ? Net::HTTP::Get : Net::HTTP::Post
|
|
91
|
+
request = request_class.new(uri.request_uri)
|
|
92
|
+
|
|
93
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
94
|
+
request["Content-Type"] = "application/json"
|
|
95
|
+
request["Accept"] = "application/json"
|
|
96
|
+
request["User-Agent"] = "stellwerk-ruby/#{Stellwerk::VERSION}"
|
|
97
|
+
|
|
98
|
+
request.body = JSON.generate(body) if body && method == :post
|
|
99
|
+
|
|
100
|
+
request
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def handle_response(response)
|
|
104
|
+
case response.code.to_i
|
|
105
|
+
when 200..299
|
|
106
|
+
parse_json(response.body)
|
|
107
|
+
when 401
|
|
108
|
+
raise ApiError.new(
|
|
109
|
+
"Invalid API key",
|
|
110
|
+
status_code: 401,
|
|
111
|
+
response_body: response.body
|
|
112
|
+
)
|
|
113
|
+
when 403
|
|
114
|
+
raise ApiError.new(
|
|
115
|
+
"API key not authorized for this operation",
|
|
116
|
+
status_code: 403,
|
|
117
|
+
response_body: response.body
|
|
118
|
+
)
|
|
119
|
+
when 429
|
|
120
|
+
raise ApiError.new(
|
|
121
|
+
"Rate limit exceeded",
|
|
122
|
+
status_code: 429,
|
|
123
|
+
response_body: response.body
|
|
124
|
+
)
|
|
125
|
+
when 500..599
|
|
126
|
+
raise ApiError.new(
|
|
127
|
+
"Stellwerk API server error",
|
|
128
|
+
status_code: response.code.to_i,
|
|
129
|
+
response_body: response.body
|
|
130
|
+
)
|
|
131
|
+
else
|
|
132
|
+
raise ApiError.new(
|
|
133
|
+
"Unexpected API response: #{response.code}",
|
|
134
|
+
status_code: response.code.to_i,
|
|
135
|
+
response_body: response.body
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def parse_json(body)
|
|
141
|
+
return {} if body.nil? || body.empty?
|
|
142
|
+
|
|
143
|
+
JSON.parse(body)
|
|
144
|
+
rescue JSON::ParserError => e
|
|
145
|
+
raise ApiError.new(
|
|
146
|
+
"Invalid JSON response from API: #{e.message}",
|
|
147
|
+
status_code: nil,
|
|
148
|
+
response_body: body
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stellwerk
|
|
4
|
+
# Configuration class for Stellwerk SDK settings
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# Stellwerk.configure do |config|
|
|
8
|
+
# config.api_key = 'sk_stellwerk_...'
|
|
9
|
+
# config.api_url = 'https://stellwerk.io'
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
class Configuration
|
|
13
|
+
# API key for license validation and metering
|
|
14
|
+
# @return [String, nil]
|
|
15
|
+
attr_accessor :api_key
|
|
16
|
+
|
|
17
|
+
# Base URL for the Stellwerk API
|
|
18
|
+
# @return [String]
|
|
19
|
+
attr_accessor :api_url
|
|
20
|
+
|
|
21
|
+
# Interval in seconds between batch metering reports
|
|
22
|
+
# @return [Integer]
|
|
23
|
+
attr_accessor :sync_interval
|
|
24
|
+
|
|
25
|
+
# Maximum seconds to allow offline operation when server is unreachable
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
attr_accessor :offline_grace_period
|
|
28
|
+
|
|
29
|
+
# Maximum executions allowed during offline grace period
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
attr_accessor :offline_grace_executions
|
|
32
|
+
|
|
33
|
+
# Logger instance
|
|
34
|
+
# @return [Logger]
|
|
35
|
+
attr_accessor :logger
|
|
36
|
+
|
|
37
|
+
# Whether to verify SSL certificates (disable for development only)
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
attr_accessor :verify_ssl
|
|
40
|
+
|
|
41
|
+
# Default API URL
|
|
42
|
+
DEFAULT_API_URL = "https://stellwerk.io"
|
|
43
|
+
|
|
44
|
+
# Default sync interval (60 seconds)
|
|
45
|
+
DEFAULT_SYNC_INTERVAL = 60
|
|
46
|
+
|
|
47
|
+
# Default offline grace period (24 hours)
|
|
48
|
+
DEFAULT_OFFLINE_GRACE_PERIOD = 86_400
|
|
49
|
+
|
|
50
|
+
# Default offline grace executions
|
|
51
|
+
DEFAULT_OFFLINE_GRACE_EXECUTIONS = 100
|
|
52
|
+
|
|
53
|
+
def initialize
|
|
54
|
+
@api_key = nil
|
|
55
|
+
@api_url = DEFAULT_API_URL
|
|
56
|
+
@sync_interval = DEFAULT_SYNC_INTERVAL
|
|
57
|
+
@offline_grace_period = DEFAULT_OFFLINE_GRACE_PERIOD
|
|
58
|
+
@offline_grace_executions = DEFAULT_OFFLINE_GRACE_EXECUTIONS
|
|
59
|
+
@logger = nil
|
|
60
|
+
@verify_ssl = true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if an API key is configured
|
|
64
|
+
# @return [Boolean]
|
|
65
|
+
def api_key?
|
|
66
|
+
!@api_key.nil? && !@api_key.empty?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Reset configuration to defaults
|
|
70
|
+
def reset!
|
|
71
|
+
@api_key = nil
|
|
72
|
+
@api_url = DEFAULT_API_URL
|
|
73
|
+
@sync_interval = DEFAULT_SYNC_INTERVAL
|
|
74
|
+
@offline_grace_period = DEFAULT_OFFLINE_GRACE_PERIOD
|
|
75
|
+
@offline_grace_executions = DEFAULT_OFFLINE_GRACE_EXECUTIONS
|
|
76
|
+
@logger = nil
|
|
77
|
+
@verify_ssl = true
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/stellwerk/errors.rb
CHANGED
|
@@ -15,4 +15,21 @@ module Stellwerk
|
|
|
15
15
|
|
|
16
16
|
# Raised when a required input is missing
|
|
17
17
|
class MissingInputError < Error; end
|
|
18
|
+
|
|
19
|
+
# Raised when API key is missing or invalid
|
|
20
|
+
class LicenseError < Error; end
|
|
21
|
+
|
|
22
|
+
# Raised when execution limit is reached
|
|
23
|
+
class QuotaExceededError < Error; end
|
|
24
|
+
|
|
25
|
+
# Raised when API communication fails
|
|
26
|
+
class ApiError < Error
|
|
27
|
+
attr_reader :status_code, :response_body
|
|
28
|
+
|
|
29
|
+
def initialize(message, status_code: nil, response_body: nil)
|
|
30
|
+
super(message)
|
|
31
|
+
@status_code = status_code
|
|
32
|
+
@response_body = response_body
|
|
33
|
+
end
|
|
34
|
+
end
|
|
18
35
|
end
|
data/lib/stellwerk/flow.rb
CHANGED
|
@@ -41,13 +41,32 @@ module Stellwerk
|
|
|
41
41
|
# @param sub_flows [Hash] Additional sub-flows for map nodes
|
|
42
42
|
# @param metadata [Hash] Additional metadata to merge into context
|
|
43
43
|
# @return [Stellwerk::Result]
|
|
44
|
+
# @raise [LicenseError] If API key is not configured or invalid
|
|
45
|
+
# @raise [QuotaExceededError] If execution limit is exceeded
|
|
44
46
|
def execute(params = {}, sub_flows: {}, metadata: {})
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
enforce_license!
|
|
48
|
+
check_quota!
|
|
49
|
+
|
|
50
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
51
|
+
status = "success"
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
result = Evaluator.call(
|
|
55
|
+
compiled_json: @compiled_json,
|
|
56
|
+
params: params,
|
|
57
|
+
sub_flows: sub_flows,
|
|
58
|
+
metadata: metadata
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
status = "error" unless result.success?
|
|
62
|
+
result
|
|
63
|
+
rescue StandardError
|
|
64
|
+
status = "error"
|
|
65
|
+
raise
|
|
66
|
+
ensure
|
|
67
|
+
duration_ms = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - start_time
|
|
68
|
+
track_execution(duration_ms, status)
|
|
69
|
+
end
|
|
51
70
|
end
|
|
52
71
|
|
|
53
72
|
# Get the flow version from compiled JSON
|
|
@@ -73,6 +92,52 @@ module Stellwerk
|
|
|
73
92
|
|
|
74
93
|
private
|
|
75
94
|
|
|
95
|
+
def enforce_license!
|
|
96
|
+
license = Stellwerk.license
|
|
97
|
+
|
|
98
|
+
# Try to validate if not yet validated or cache expired
|
|
99
|
+
unless license.valid?
|
|
100
|
+
license.validate!
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check again after validation attempt
|
|
104
|
+
unless license.valid?
|
|
105
|
+
raise LicenseError, "Invalid or expired license"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def check_quota!
|
|
110
|
+
license = Stellwerk.license
|
|
111
|
+
|
|
112
|
+
unless license.within_limits?
|
|
113
|
+
if license.offline?
|
|
114
|
+
raise QuotaExceededError,
|
|
115
|
+
"Offline execution limit reached. Connect to the network to continue."
|
|
116
|
+
else
|
|
117
|
+
raise QuotaExceededError,
|
|
118
|
+
"Execution limit reached for your plan. Upgrade at https://stellwerk.io"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def track_execution(duration_ms, status)
|
|
124
|
+
license = Stellwerk.license
|
|
125
|
+
|
|
126
|
+
# Record offline execution if in grace period
|
|
127
|
+
license.record_offline_execution if license.offline?
|
|
128
|
+
|
|
129
|
+
# Track in metering system
|
|
130
|
+
Stellwerk.metering.track(
|
|
131
|
+
flow_id: flow_id,
|
|
132
|
+
duration_ms: duration_ms,
|
|
133
|
+
status: status
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def flow_id
|
|
138
|
+
@compiled_json["id"] || @compiled_json[:id] || @path || "unknown"
|
|
139
|
+
end
|
|
140
|
+
|
|
76
141
|
def validate!
|
|
77
142
|
nodes = @compiled_json["nodes"] || @compiled_json[:nodes]
|
|
78
143
|
unless nodes.is_a?(Hash)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stellwerk
|
|
4
|
+
# License validation and caching
|
|
5
|
+
#
|
|
6
|
+
# Validates API keys against the Stellwerk server and caches the result.
|
|
7
|
+
# Supports offline grace periods when the server is unreachable.
|
|
8
|
+
#
|
|
9
|
+
# Thread-safe via Mutex.
|
|
10
|
+
#
|
|
11
|
+
class License
|
|
12
|
+
# Cached license data attributes
|
|
13
|
+
attr_reader :plan, :execution_limit, :executions_used, :expires_at
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@validated = false
|
|
18
|
+
@valid = false
|
|
19
|
+
@plan = nil
|
|
20
|
+
@execution_limit = 0
|
|
21
|
+
@executions_used = 0
|
|
22
|
+
@expires_at = nil
|
|
23
|
+
@offline_since = nil
|
|
24
|
+
@offline_executions = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Validate the API key against the server
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean] Whether the license is valid
|
|
30
|
+
# @raise [LicenseError] If API key is missing
|
|
31
|
+
# @raise [ApiError] On server communication failure (after grace period)
|
|
32
|
+
def validate!
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
validate_internal!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if the current license is valid (cached)
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean]
|
|
41
|
+
def valid?
|
|
42
|
+
@mutex.synchronize do
|
|
43
|
+
return false unless @validated
|
|
44
|
+
return @valid if within_cache_ttl?
|
|
45
|
+
return in_offline_grace? if @offline_since
|
|
46
|
+
|
|
47
|
+
@valid
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Refresh the license if TTL has expired
|
|
52
|
+
#
|
|
53
|
+
# @return [Boolean] Whether the license is valid after refresh
|
|
54
|
+
def refresh_if_needed
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
return @valid if within_cache_ttl?
|
|
57
|
+
|
|
58
|
+
validate_internal!
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check if executions are within allowed limits
|
|
63
|
+
#
|
|
64
|
+
# @param pending_count [Integer] Number of pending executions to check
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def within_limits?(pending_count = 1)
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
return false unless @valid || in_offline_grace?
|
|
69
|
+
|
|
70
|
+
if @offline_since
|
|
71
|
+
@offline_executions + pending_count <= offline_grace_executions
|
|
72
|
+
else
|
|
73
|
+
@executions_used + pending_count <= @execution_limit
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Record an offline execution during grace period
|
|
79
|
+
def record_offline_execution
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
@offline_executions += 1
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Update execution count from server response
|
|
86
|
+
#
|
|
87
|
+
# @param count [Integer] Current execution count from server
|
|
88
|
+
def update_executions_used(count)
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@executions_used = count
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Check if in offline mode
|
|
95
|
+
#
|
|
96
|
+
# @return [Boolean]
|
|
97
|
+
def offline?
|
|
98
|
+
@mutex.synchronize do
|
|
99
|
+
!@offline_since.nil?
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Reset the license state
|
|
104
|
+
def reset!
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
@validated = false
|
|
107
|
+
@valid = false
|
|
108
|
+
@plan = nil
|
|
109
|
+
@execution_limit = 0
|
|
110
|
+
@executions_used = 0
|
|
111
|
+
@expires_at = nil
|
|
112
|
+
@offline_since = nil
|
|
113
|
+
@offline_executions = 0
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def validate_internal!
|
|
120
|
+
config = Stellwerk.configuration
|
|
121
|
+
|
|
122
|
+
unless config.api_key?
|
|
123
|
+
raise LicenseError, "API key is required. Set Stellwerk.configuration.api_key"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
client = ApiClient.new(api_url: config.api_url, api_key: config.api_key, verify_ssl: config.verify_ssl)
|
|
128
|
+
response = client.post("/api/v1/sdk/validate")
|
|
129
|
+
|
|
130
|
+
apply_validation_response(response)
|
|
131
|
+
clear_offline_state
|
|
132
|
+
@validated = true
|
|
133
|
+
@valid
|
|
134
|
+
rescue ApiError => e
|
|
135
|
+
handle_validation_error(e)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def apply_validation_response(response)
|
|
140
|
+
@valid = response["valid"] == true
|
|
141
|
+
@plan = response["plan"]
|
|
142
|
+
|
|
143
|
+
limits = response["limits"] || {}
|
|
144
|
+
@execution_limit = limits["monthly_executions"].to_i
|
|
145
|
+
@executions_used = limits["current_count"].to_i
|
|
146
|
+
|
|
147
|
+
cache_ttl = response["cache_ttl"].to_i
|
|
148
|
+
cache_ttl = 3600 if cache_ttl <= 0
|
|
149
|
+
@expires_at = Time.now + cache_ttl
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def handle_validation_error(error)
|
|
153
|
+
if error.status_code == 401
|
|
154
|
+
@valid = false
|
|
155
|
+
@validated = true
|
|
156
|
+
raise LicenseError, "Invalid API key"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Server unreachable - enter or continue offline grace period
|
|
160
|
+
if @validated && @valid
|
|
161
|
+
enter_offline_grace unless @offline_since
|
|
162
|
+
Stellwerk.logger.warn("License server unreachable, operating in offline grace mode")
|
|
163
|
+
return in_offline_grace?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Never validated successfully - cannot enter grace period
|
|
167
|
+
raise LicenseError, "Unable to validate license: #{error.message}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def enter_offline_grace
|
|
171
|
+
@offline_since = Time.now
|
|
172
|
+
@offline_executions = 0
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def clear_offline_state
|
|
176
|
+
@offline_since = nil
|
|
177
|
+
@offline_executions = 0
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def within_cache_ttl?
|
|
181
|
+
@expires_at && Time.now < @expires_at
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def in_offline_grace?
|
|
185
|
+
return false unless @offline_since
|
|
186
|
+
|
|
187
|
+
within_time = (Time.now - @offline_since) < offline_grace_period
|
|
188
|
+
within_executions = @offline_executions < offline_grace_executions
|
|
189
|
+
|
|
190
|
+
within_time && within_executions
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def offline_grace_period
|
|
194
|
+
Stellwerk.configuration.offline_grace_period
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def offline_grace_executions
|
|
198
|
+
Stellwerk.configuration.offline_grace_executions
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Stellwerk
|
|
4
|
+
# Execution metering and batch reporting
|
|
5
|
+
#
|
|
6
|
+
# Tracks flow executions locally and periodically reports them
|
|
7
|
+
# to the Stellwerk server in batches for efficiency.
|
|
8
|
+
#
|
|
9
|
+
# Thread-safe via Mutex.
|
|
10
|
+
#
|
|
11
|
+
class Metering
|
|
12
|
+
# Execution record structure
|
|
13
|
+
Execution = Struct.new(:flow_id, :timestamp, :duration_ms, :status, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
@pending_executions = []
|
|
18
|
+
@last_flush_at = Time.now
|
|
19
|
+
@background_thread = nil
|
|
20
|
+
@running = false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Track a flow execution
|
|
24
|
+
#
|
|
25
|
+
# @param flow_id [String] Identifier for the flow
|
|
26
|
+
# @param duration_ms [Integer] Execution duration in milliseconds
|
|
27
|
+
# @param status [String] Execution status ('success', 'error')
|
|
28
|
+
def track(flow_id:, duration_ms:, status:)
|
|
29
|
+
execution = Execution.new(
|
|
30
|
+
flow_id: flow_id,
|
|
31
|
+
timestamp: Time.now.utc.iso8601,
|
|
32
|
+
duration_ms: duration_ms,
|
|
33
|
+
status: status
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@pending_executions << execution
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
auto_flush_if_needed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Flush pending executions to the server
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash, nil] Server response or nil if nothing to flush
|
|
46
|
+
def flush
|
|
47
|
+
executions_to_send = nil
|
|
48
|
+
|
|
49
|
+
@mutex.synchronize do
|
|
50
|
+
return nil if @pending_executions.empty?
|
|
51
|
+
|
|
52
|
+
executions_to_send = @pending_executions.dup
|
|
53
|
+
@pending_executions.clear
|
|
54
|
+
@last_flush_at = Time.now
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
send_executions(executions_to_send)
|
|
58
|
+
rescue ApiError => e
|
|
59
|
+
# Re-queue executions on failure
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
@pending_executions = executions_to_send + @pending_executions
|
|
62
|
+
end
|
|
63
|
+
Stellwerk.logger.error("Failed to report executions: #{e.message}")
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Get count of pending executions
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer]
|
|
70
|
+
def pending_count
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
@pending_executions.length
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Start background auto-flush thread
|
|
77
|
+
#
|
|
78
|
+
# @return [Thread]
|
|
79
|
+
def start_background_flush
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
return @background_thread if @running
|
|
82
|
+
|
|
83
|
+
@running = true
|
|
84
|
+
@background_thread = Thread.new { background_flush_loop }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Stop background auto-flush thread
|
|
89
|
+
def stop_background_flush
|
|
90
|
+
@mutex.synchronize do
|
|
91
|
+
@running = false
|
|
92
|
+
end
|
|
93
|
+
@background_thread&.join(5)
|
|
94
|
+
@background_thread = nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if background flush is running
|
|
98
|
+
#
|
|
99
|
+
# @return [Boolean]
|
|
100
|
+
def background_flush_running?
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
@running && @background_thread&.alive?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Reset metering state
|
|
107
|
+
def reset!
|
|
108
|
+
stop_background_flush
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
@pending_executions.clear
|
|
111
|
+
@last_flush_at = Time.now
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def auto_flush_if_needed
|
|
118
|
+
should_flush = @mutex.synchronize do
|
|
119
|
+
time_since_flush = Time.now - @last_flush_at
|
|
120
|
+
time_since_flush >= sync_interval
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
flush if should_flush
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def background_flush_loop
|
|
127
|
+
while @mutex.synchronize { @running }
|
|
128
|
+
sleep(sync_interval)
|
|
129
|
+
flush if @mutex.synchronize { @running }
|
|
130
|
+
end
|
|
131
|
+
rescue StandardError => e
|
|
132
|
+
Stellwerk.logger.error("Background flush error: #{e.message}")
|
|
133
|
+
@mutex.synchronize { @running = false }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def send_executions(executions)
|
|
137
|
+
config = Stellwerk.configuration
|
|
138
|
+
return nil unless config.api_key?
|
|
139
|
+
|
|
140
|
+
client = ApiClient.new(api_url: config.api_url, api_key: config.api_key, verify_ssl: config.verify_ssl)
|
|
141
|
+
|
|
142
|
+
body = {
|
|
143
|
+
executions: executions.map do |exec|
|
|
144
|
+
{
|
|
145
|
+
flow_id: exec.flow_id,
|
|
146
|
+
timestamp: exec.timestamp,
|
|
147
|
+
duration_ms: exec.duration_ms,
|
|
148
|
+
status: exec.status
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
response = client.post("/api/v1/executions/report", body)
|
|
154
|
+
|
|
155
|
+
# Update license with current usage from response
|
|
156
|
+
limits = response["limits"] || {}
|
|
157
|
+
if limits["current_count"]
|
|
158
|
+
Stellwerk.license.update_executions_used(limits["current_count"])
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
response
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def sync_interval
|
|
165
|
+
Stellwerk.configuration.sync_interval
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
data/lib/stellwerk/version.rb
CHANGED
data/lib/stellwerk.rb
CHANGED
|
@@ -4,6 +4,10 @@ require "logger"
|
|
|
4
4
|
|
|
5
5
|
require_relative "stellwerk/version"
|
|
6
6
|
require_relative "stellwerk/errors"
|
|
7
|
+
require_relative "stellwerk/configuration"
|
|
8
|
+
require_relative "stellwerk/api_client"
|
|
9
|
+
require_relative "stellwerk/license"
|
|
10
|
+
require_relative "stellwerk/metering"
|
|
7
11
|
require_relative "stellwerk/result"
|
|
8
12
|
require_relative "stellwerk/functions"
|
|
9
13
|
require_relative "stellwerk/evaluator"
|
|
@@ -37,24 +41,83 @@ require_relative "stellwerk/flow"
|
|
|
37
41
|
#
|
|
38
42
|
module Stellwerk
|
|
39
43
|
class << self
|
|
40
|
-
#
|
|
41
|
-
|
|
44
|
+
# Returns the configuration instance
|
|
45
|
+
#
|
|
46
|
+
# @return [Configuration]
|
|
47
|
+
def configuration
|
|
48
|
+
@configuration ||= Configuration.new
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Configure Stellwerk
|
|
52
|
+
#
|
|
53
|
+
# @yield [Configuration] Yields the configuration instance
|
|
54
|
+
# @example
|
|
55
|
+
# Stellwerk.configure do |config|
|
|
56
|
+
# config.api_key = 'sk_stellwerk_...'
|
|
57
|
+
# config.logger = Logger.new(STDOUT)
|
|
58
|
+
# end
|
|
59
|
+
def configure
|
|
60
|
+
yield configuration
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Convenience setter for API key
|
|
64
|
+
#
|
|
65
|
+
# @param key [String] The API key
|
|
66
|
+
def api_key=(key)
|
|
67
|
+
configuration.api_key = key
|
|
68
|
+
end
|
|
42
69
|
|
|
43
70
|
# Returns the configured logger (defaults to a null logger)
|
|
71
|
+
#
|
|
72
|
+
# @return [Logger]
|
|
44
73
|
def logger
|
|
45
|
-
|
|
74
|
+
configuration.logger || @default_logger ||= Logger.new(File::NULL)
|
|
46
75
|
end
|
|
47
76
|
|
|
48
|
-
#
|
|
77
|
+
# Set the logger
|
|
49
78
|
#
|
|
50
|
-
# @
|
|
51
|
-
def
|
|
52
|
-
|
|
79
|
+
# @param logger [Logger]
|
|
80
|
+
def logger=(logger)
|
|
81
|
+
configuration.logger = logger
|
|
53
82
|
end
|
|
54
83
|
|
|
55
|
-
#
|
|
84
|
+
# Returns the license instance
|
|
85
|
+
#
|
|
86
|
+
# @return [License]
|
|
87
|
+
def license
|
|
88
|
+
@license ||= License.new
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the metering instance
|
|
92
|
+
#
|
|
93
|
+
# @return [Metering]
|
|
94
|
+
def metering
|
|
95
|
+
@metering ||= Metering.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Check if the SDK is properly licensed
|
|
99
|
+
#
|
|
100
|
+
# Validates the API key on first call and caches the result.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def licensed?
|
|
104
|
+
return false unless configuration.api_key?
|
|
105
|
+
|
|
106
|
+
license.valid? || license.validate!
|
|
107
|
+
rescue LicenseError, ApiError
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Reset all state to defaults
|
|
112
|
+
#
|
|
113
|
+
# Resets configuration, license, and metering state.
|
|
56
114
|
def reset!
|
|
57
|
-
|
|
115
|
+
metering.reset!
|
|
116
|
+
license.reset!
|
|
117
|
+
@configuration = nil
|
|
118
|
+
@license = nil
|
|
119
|
+
@metering = nil
|
|
120
|
+
@default_logger = nil
|
|
58
121
|
end
|
|
59
122
|
end
|
|
60
123
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: stellwerk-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roadwerk
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dentaku
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '3.12'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: webmock
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.18'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.18'
|
|
69
83
|
description: |
|
|
70
84
|
Stellwerk is a standalone Ruby gem for evaluating pre-compiled flow JSON
|
|
71
85
|
without any Rails or ActiveRecord dependencies. It provides a fast, portable
|
|
@@ -80,10 +94,14 @@ files:
|
|
|
80
94
|
- LICENSE.txt
|
|
81
95
|
- README.md
|
|
82
96
|
- lib/stellwerk.rb
|
|
97
|
+
- lib/stellwerk/api_client.rb
|
|
98
|
+
- lib/stellwerk/configuration.rb
|
|
83
99
|
- lib/stellwerk/errors.rb
|
|
84
100
|
- lib/stellwerk/evaluator.rb
|
|
85
101
|
- lib/stellwerk/flow.rb
|
|
86
102
|
- lib/stellwerk/functions.rb
|
|
103
|
+
- lib/stellwerk/license.rb
|
|
104
|
+
- lib/stellwerk/metering.rb
|
|
87
105
|
- lib/stellwerk/result.rb
|
|
88
106
|
- lib/stellwerk/version.rb
|
|
89
107
|
homepage: https://github.com/roadwerk/stellwerk-ruby
|