trigv 1.0.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: db623af95b6d63ba76f6aadc73ad1fe6783aae9d60a7af01e15be026aa7840a3
4
+ data.tar.gz: 72082f5c52bf1e92d207932e646e7478cbc69ff9db4dd62fa348518debdc6e3c
5
+ SHA512:
6
+ metadata.gz: bd42a53191af4b90172976e1c0adbb1c0b8a0499f4eae0781676df2793bab349b837efe189fa2d35773893dac7516658c725076ceec27ee56e2c7acfc99e992f
7
+ data.tar.gz: 942bb3b48a9f4ddd77b6544f0a7d50a442a19c06135da3ff5e4a87178f88014b5c1bc3989444f2dca8d3161cb5d72e431447eedaca0141e47c2e0ef9effa2b7a
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-07-07
9
+
10
+ ### Added
11
+
12
+ - Initial stable release of the `trigv` gem
13
+ - `Trigv::Client` with `#send_event` and `#verify_connection`
14
+ - Net::HTTP client, strict field validation, WebMock tests
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Trigv
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # trigv
2
+
3
+ Official Trigv SDK for Ruby. Send notification events from scripts, servers, and CI pipelines.
4
+
5
+ ## What Trigv is
6
+
7
+ Trigv delivers developer notifications to your team's devices. Your backend sends lightweight JSON events; Trigv queues push delivery. Notification title and body are not stored on Trigv servers — metadata only.
8
+
9
+ ## Installation
10
+
11
+ Add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem 'trigv', '~> 0.1'
15
+ ```
16
+
17
+ Then run:
18
+
19
+ ```bash
20
+ bundle install
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```ruby
26
+ require 'trigv'
27
+
28
+ client = Trigv::Client.new(api_key: ENV['TRIGV_API_KEY'])
29
+
30
+ client.send_event(
31
+ channel: 'general',
32
+ title: 'Deploy finished',
33
+ description: 'Build #42 succeeded in 38s'
34
+ )
35
+ ```
36
+
37
+ ## Authentication
38
+
39
+ Create a workspace API key at [app.trigv.com](https://app.trigv.com). Keys look like `trgv_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
40
+
41
+ ```ruby
42
+ client = Trigv::Client.new(api_key: 'trgv_your_api_key')
43
+ # or set TRIGV_API_KEY in the environment
44
+ ```
45
+
46
+ **Do not** send the API key in the JSON body — use the `Authorization: Bearer` header (handled automatically).
47
+
48
+ ## Sending an event
49
+
50
+ ```ruby
51
+ result = client.send_event(
52
+ channel: 'general',
53
+ title: 'Cron job failed',
54
+ description: 'Nightly backup did not complete',
55
+ level: 'error',
56
+ event_type: 'cron.failed'
57
+ )
58
+
59
+ puts result.event.public_id
60
+ ```
61
+
62
+ ## Event options
63
+
64
+ | Field | Required | Description |
65
+ |-------|:--------:|-------------|
66
+ | `channel` | Yes | Channel slug (e.g. `general`) |
67
+ | `title` | Yes | Notification title |
68
+ | `description` | No | Body text |
69
+ | `image_url` | No | HTTPS image URL (not stored server-side) |
70
+ | `url` | No | Destination URL for the notification (max 2048 characters; not stored server-side) |
71
+ | `level` | No | `info`, `success`, `warning`, `error` (default: `info`) |
72
+ | `delivery_urgency` | No | `standard` or `time_sensitive` (default: `standard`) |
73
+ | `event_type` | No | Free-form label (e.g. `deploy.completed`) |
74
+ | `idempotency_key` | No | Dedup key per workspace |
75
+
76
+ ## Levels
77
+
78
+ - `info` — general information (default)
79
+ - `success` — completed successfully
80
+ - `warning` — attention needed
81
+ - `error` — failure or alert
82
+
83
+ ## Delivery urgency
84
+
85
+ - `standard` — normal notifications (default)
86
+ - `time_sensitive` — iOS Time Sensitive delivery (requires user permission)
87
+
88
+ ## Idempotency
89
+
90
+ When you set `idempotency_key`, retries with the same key return the existing event (`duplicate: true`, HTTP 200) without billing again.
91
+
92
+ ```ruby
93
+ result = client.send_event(
94
+ channel: 'deploys',
95
+ title: 'Production deploy complete',
96
+ idempotency_key: 'deploy-prod-42'
97
+ )
98
+
99
+ puts result.duplicate ? 'duplicate' : 'new'
100
+ ```
101
+
102
+ ## Error handling
103
+
104
+ ```ruby
105
+ begin
106
+ client.send_event(channel: 'general', title: 'Hello')
107
+ rescue Trigv::ValidationError => e
108
+ puts e.errors
109
+ rescue Trigv::NotFoundError
110
+ puts 'Channel not found'
111
+ rescue Trigv::RateLimitError => e
112
+ puts e.retryable ? 'Retry later' : 'Monthly limit reached'
113
+ end
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ | Option | Default | Description |
119
+ |--------|---------|-------------|
120
+ | `api_key` | `TRIGV_API_KEY` env | Workspace API key |
121
+ | `base_url` | `https://api.trigv.com/api` | API base URL |
122
+ | `timeout` | `30` | Request timeout (seconds) |
123
+ | `max_retries` | `2` | Retries for retryable errors |
124
+
125
+ ### Verify connection
126
+
127
+ ```ruby
128
+ connection = client.verify_connection
129
+ puts connection.workspace.name
130
+ ```
131
+
132
+ ## Examples
133
+
134
+ See the [`examples/`](./examples/) directory:
135
+
136
+ - Basic event
137
+ - Success event
138
+ - Warning event
139
+ - Full event with all optional fields
140
+ - Idempotency example
141
+ - Deploy completed (canonical example)
142
+ - Cron job failed
143
+ - WooCommerce order notification
144
+ - AI agent task completed
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ bundle install
150
+ bundle exec rspec
151
+ bundle exec rubocop
152
+ ```
153
+
154
+ ## Testing
155
+
156
+ ```bash
157
+ bundle exec rspec
158
+ ```
159
+
160
+ Tests use WebMock — no live API key required.
161
+
162
+ ## Contributing
163
+
164
+ Contributions welcome. Please open an issue before large changes.
165
+
166
+ ## Licence
167
+
168
+ MIT
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ require_relative 'retry'
8
+
9
+ module Trigv
10
+ Event = Struct.new(
11
+ :public_id, :event_uuid, :status, :level, :event_type, :target_device_count, :received_at,
12
+ keyword_init: true
13
+ )
14
+
15
+ SendEventResult = Struct.new(:event, :duplicate, keyword_init: true)
16
+
17
+ ConnectionWorkspace = Struct.new(:public_id, :name, keyword_init: true)
18
+ ConnectionApiKey = Struct.new(:public_id, :name, :prefix, keyword_init: true)
19
+ ConnectionResult = Struct.new(:workspace, :api_key, keyword_init: true)
20
+
21
+ class Client
22
+ attr_reader :api_key, :base_url, :timeout, :max_retries
23
+
24
+ def initialize(
25
+ api_key: nil,
26
+ base_url: Validation::DEFAULT_BASE_URL,
27
+ timeout: Validation::DEFAULT_TIMEOUT,
28
+ max_retries: Validation::MAX_RETRIES
29
+ )
30
+ api_key ||= ENV.fetch('TRIGV_API_KEY', nil)
31
+ api_key = api_key&.strip
32
+ raise ValidationError, 'api_key is required (or set TRIGV_API_KEY)' if api_key.nil? || api_key.empty?
33
+ raise ValidationError, 'max_retries must be zero or greater' if max_retries.negative?
34
+ raise ValidationError, 'timeout must be greater than zero' if timeout <= 0
35
+
36
+ @api_key = api_key
37
+ @base_url = base_url.to_s.chomp('/')
38
+ @timeout = timeout
39
+ @max_retries = max_retries
40
+ end
41
+
42
+ def user_agent
43
+ "trigv-ruby/#{VERSION}"
44
+ end
45
+
46
+ def send_event(**params)
47
+ body = Validation.validate_send_event_params(params)
48
+ has_idempotency = body.key?('idempotency_key')
49
+
50
+ request_with_retry(:post, '/v1/events', body, allow_retry: has_idempotency) do |status, parsed, headers|
51
+ if [200, 202].include?(status) && parsed&.dig('event')
52
+ event = parse_event(parsed['event'])
53
+ SendEventResult.new(event: event, duplicate: status == 200)
54
+ else
55
+ error_from_response(status, parsed, headers)
56
+ end
57
+ end
58
+ end
59
+
60
+ def verify_connection
61
+ request_with_retry(:get, '/v1/connection', nil, allow_retry: true) do |status, parsed, headers|
62
+ if status == 200 && parsed&.dig('workspace') && parsed['api_key']
63
+ parse_connection_result(parsed)
64
+ else
65
+ error_from_response(status, parsed, headers)
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def request_with_retry(method, path, body, allow_retry:)
73
+ last_error = nil
74
+
75
+ (0..max_retries).each do |attempt|
76
+ status, parsed, headers = request_once(method, path, body)
77
+ return yield(status, parsed, headers)
78
+ rescue StandardError => e
79
+ last_error = e
80
+ raise e unless allow_retry && attempt < max_retries
81
+ raise e unless retryable_error?(e)
82
+
83
+ wait = retry_wait_seconds(e, attempt)
84
+ sleep([wait, timeout].min)
85
+ end
86
+
87
+ raise last_error
88
+ end
89
+
90
+ def retry_wait_seconds(error, attempt)
91
+ return error.retry_after_seconds if error.is_a?(RateLimitError) && error.retry_after_seconds
92
+
93
+ (0.5 * (2**attempt)) + (rand * 0.25)
94
+ end
95
+
96
+ def retryable_error?(error)
97
+ case error
98
+ when ValidationError, AuthenticationError, AuthorizationError, NotFoundError
99
+ false
100
+ when RateLimitError
101
+ error.retryable
102
+ when ApiError
103
+ retryable_status?(error.status_code)
104
+ when NetworkError, TimeoutError
105
+ true
106
+ else
107
+ false
108
+ end
109
+ end
110
+
111
+ def retryable_status?(status)
112
+ [429, 502, 503, 504].include?(status)
113
+ end
114
+
115
+ def request_once(method, path, body)
116
+ uri = build_uri(path)
117
+ http = build_http(uri)
118
+ request = build_request(method, uri, body)
119
+ response = http.request(request)
120
+ parsed = parse_json(response.body)
121
+ response_headers = response.each_header.to_h.transform_keys(&:downcase)
122
+
123
+ [response.code.to_i, parsed, response_headers]
124
+ rescue Net::OpenTimeout, Net::ReadTimeout, Timeout::Error
125
+ raise TimeoutError
126
+ rescue ValidationError, AuthenticationError, AuthorizationError, NotFoundError,
127
+ RateLimitError, ApiError, TimeoutError
128
+ raise
129
+ rescue StandardError => e
130
+ raise NetworkError.new(e.message, e)
131
+ end
132
+
133
+ def build_uri(path)
134
+ URI("#{@base_url}#{path}")
135
+ end
136
+
137
+ def build_http(uri)
138
+ http = Net::HTTP.new(uri.host, uri.port)
139
+ http.use_ssl = (uri.scheme == 'https')
140
+ http.open_timeout = timeout
141
+ http.read_timeout = timeout
142
+ http
143
+ end
144
+
145
+ def build_request(method, uri, body)
146
+ request =
147
+ case method
148
+ when :get then Net::HTTP::Get.new(uri)
149
+ when :post then Net::HTTP::Post.new(uri)
150
+ else
151
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
152
+ end
153
+
154
+ request['Accept'] = 'application/json'
155
+ request['Authorization'] = "Bearer #{@api_key}"
156
+ request['User-Agent'] = user_agent
157
+
158
+ if body
159
+ request['Content-Type'] = 'application/json'
160
+ request.body = JSON.generate(body)
161
+ end
162
+
163
+ request
164
+ end
165
+
166
+ def parse_json(text)
167
+ return nil if text.nil? || text.empty?
168
+
169
+ JSON.parse(text)
170
+ rescue JSON::ParserError
171
+ nil
172
+ end
173
+
174
+ def parse_event(data)
175
+ Event.new(
176
+ public_id: data['public_id'],
177
+ event_uuid: data['event_uuid'],
178
+ status: data['status'],
179
+ level: data['level'],
180
+ event_type: data['event_type'],
181
+ target_device_count: data['target_device_count'],
182
+ received_at: data['received_at']
183
+ )
184
+ end
185
+
186
+ def parse_connection_result(parsed)
187
+ workspace_data = parsed['workspace']
188
+ api_key_data = parsed['api_key']
189
+
190
+ unless workspace_data.is_a?(Hash) && api_key_data.is_a?(Hash)
191
+ raise ValidationError.new('Invalid connection response payload', {})
192
+ end
193
+
194
+ required_workspace = %w[public_id name]
195
+ required_api_key = %w[public_id name prefix]
196
+ missing_workspace = required_workspace.reject { |key| workspace_data[key].is_a?(String) && !workspace_data[key].empty? }
197
+ missing_api_key = required_api_key.reject { |key| api_key_data[key].is_a?(String) && !api_key_data[key].empty? }
198
+
199
+ unless missing_workspace.empty? && missing_api_key.empty?
200
+ raise ValidationError.new('Invalid connection response payload', {})
201
+ end
202
+
203
+ ConnectionResult.new(
204
+ workspace: ConnectionWorkspace.new(**symbolize_keys(workspace_data)),
205
+ api_key: ConnectionApiKey.new(**symbolize_keys(api_key_data))
206
+ )
207
+ rescue ArgumentError, KeyError, TypeError
208
+ raise ValidationError.new('Invalid connection response payload', {})
209
+ end
210
+
211
+ def symbolize_keys(hash)
212
+ hash.transform_keys(&:to_sym)
213
+ end
214
+
215
+ def error_from_response(status, body, headers = {})
216
+ message = body&.fetch('message', nil) || "Trigv API returned HTTP #{status}"
217
+
218
+ case status
219
+ when 401
220
+ raise AuthenticationError, message
221
+ when 403
222
+ raise AuthorizationError, message
223
+ when 404
224
+ raise NotFoundError, message
225
+ when 422
226
+ raise ValidationError.new(message, body&.fetch('errors', {}) || {})
227
+ when 429
228
+ retry_after = Retry.parse_retry_after(headers['retry-after'])
229
+ raise RateLimitError.new(
230
+ message,
231
+ retryable: !Errors.monthly_limit_message?(message),
232
+ retry_after_seconds: retry_after
233
+ )
234
+ else
235
+ raise ApiError.new(message, status, body)
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trigv
4
+ class Error < StandardError
5
+ def initialize(message = nil)
6
+ super
7
+ end
8
+ end
9
+
10
+ class ValidationError < Error
11
+ attr_reader :errors
12
+
13
+ def initialize(message, errors = {})
14
+ super(message)
15
+ @errors = errors
16
+ end
17
+ end
18
+
19
+ class AuthenticationError < Error
20
+ attr_reader :status_code
21
+
22
+ def initialize(message)
23
+ super
24
+ @status_code = 401
25
+ end
26
+ end
27
+
28
+ class AuthorizationError < Error
29
+ attr_reader :status_code
30
+
31
+ def initialize(message)
32
+ super
33
+ @status_code = 403
34
+ end
35
+ end
36
+
37
+ class NotFoundError < Error
38
+ attr_reader :status_code
39
+
40
+ def initialize(message)
41
+ super
42
+ @status_code = 404
43
+ end
44
+ end
45
+
46
+ class RateLimitError < Error
47
+ attr_reader :status_code, :retryable, :retry_after_seconds
48
+
49
+ def initialize(message, retryable: true, retry_after_seconds: nil)
50
+ super(message)
51
+ @status_code = 429
52
+ @retryable = retryable
53
+ @retry_after_seconds = retry_after_seconds
54
+ end
55
+ end
56
+
57
+ class ApiError < Error
58
+ attr_reader :status_code, :body
59
+
60
+ def initialize(message, status_code, body = nil)
61
+ super(message)
62
+ @status_code = status_code
63
+ @body = body
64
+ end
65
+ end
66
+
67
+ class NetworkError < Error
68
+ attr_reader :cause
69
+
70
+ def initialize(message, cause = nil)
71
+ super
72
+ @cause = cause
73
+ end
74
+ end
75
+
76
+ class TimeoutError < Error
77
+ def initialize(message = 'Request timed out')
78
+ super
79
+ end
80
+ end
81
+
82
+ module Errors
83
+ module_function
84
+
85
+ def monthly_limit_message?(message)
86
+ message.include?('Monthly event limit')
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Trigv
6
+ module Retry
7
+ module_function
8
+
9
+ def parse_retry_after(value, now = Time.now)
10
+ return nil if value.nil? || value.strip.empty?
11
+
12
+ trimmed = value.strip
13
+
14
+ if trimmed.match?(/\A\d+\z/)
15
+ seconds = trimmed.to_i
16
+ return nil if seconds.negative?
17
+
18
+ return seconds.to_f
19
+ end
20
+
21
+ retry_at = Time.httpdate(trimmed)
22
+ wait = retry_at - now
23
+ wait.negative? ? nil : wait
24
+ rescue ArgumentError
25
+ nil
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trigv
4
+ module Validation
5
+ DEFAULT_BASE_URL = 'https://api.trigv.com/api'
6
+ DEFAULT_TIMEOUT = 30
7
+ MAX_RETRIES = 2
8
+
9
+ LEVELS = %w[info success warning error].freeze
10
+ DELIVERY_URGENCIES = %w[standard time_sensitive].freeze
11
+
12
+ FIELD_MAX_LENGTHS = {
13
+ channel: 120,
14
+ title: 255,
15
+ description: 1000,
16
+ image_url: 2048,
17
+ url: 2048,
18
+ event_type: 120,
19
+ idempotency_key: 190
20
+ }.freeze
21
+
22
+ PROHIBITED_FIELDS = %w[api_key icon].freeze
23
+
24
+ ALLOWED_EVENT_KEYS = %i[
25
+ channel title description image_url url level delivery_urgency event_type idempotency_key
26
+ ].freeze
27
+
28
+ module_function
29
+
30
+ def validate_send_event_params(params)
31
+ params = params.transform_keys(&:to_sym)
32
+
33
+ unknown = params.keys - ALLOWED_EVENT_KEYS
34
+ unless unknown.empty?
35
+ field = unknown.first.to_s
36
+ raise ValidationError.new("Unknown field \"#{field}\"", { field => ["Unknown field \"#{field}\""] })
37
+ end
38
+
39
+ PROHIBITED_FIELDS.each do |prohibited|
40
+ next unless params.key?(prohibited.to_sym)
41
+
42
+ raise ValidationError.new(
43
+ "Field \"#{prohibited}\" is not allowed",
44
+ { prohibited => ["The #{prohibited} field is prohibited"] }
45
+ )
46
+ end
47
+
48
+ channel = trim(params[:channel])
49
+ title = trim(params[:title])
50
+
51
+ raise ValidationError.new('channel is required', { channel: ['channel is required'] }) if channel.nil?
52
+ raise ValidationError.new('title is required', { title: ['title is required'] }) if title.nil?
53
+
54
+ assert_max_length('channel', channel, FIELD_MAX_LENGTHS[:channel])
55
+ assert_max_length('title', title, FIELD_MAX_LENGTHS[:title])
56
+
57
+ body = { 'channel' => channel, 'title' => title }
58
+
59
+ description = trim(params[:description])
60
+ if description
61
+ assert_max_length('description', description, FIELD_MAX_LENGTHS[:description])
62
+ body['description'] = description
63
+ end
64
+
65
+ image_url = trim(params[:image_url])
66
+ if image_url
67
+ assert_max_length('image_url', image_url, FIELD_MAX_LENGTHS[:image_url])
68
+ body['image_url'] = image_url
69
+ end
70
+
71
+ url = trim(params[:url])
72
+ if url
73
+ assert_max_length('url', url, FIELD_MAX_LENGTHS[:url])
74
+ body['url'] = url
75
+ end
76
+
77
+ level = trim(params[:level])
78
+ if level
79
+ assert_enum('level', level, LEVELS)
80
+ body['level'] = level
81
+ end
82
+
83
+ delivery_urgency = trim(params[:delivery_urgency])
84
+ if delivery_urgency
85
+ assert_enum('delivery_urgency', delivery_urgency, DELIVERY_URGENCIES)
86
+ body['delivery_urgency'] = delivery_urgency
87
+ end
88
+
89
+ event_type = trim(params[:event_type])
90
+ if event_type
91
+ assert_max_length('event_type', event_type, FIELD_MAX_LENGTHS[:event_type])
92
+ body['event_type'] = event_type
93
+ end
94
+
95
+ idempotency_key = trim(params[:idempotency_key])
96
+ if idempotency_key
97
+ assert_max_length('idempotency_key', idempotency_key, FIELD_MAX_LENGTHS[:idempotency_key])
98
+ body['idempotency_key'] = idempotency_key
99
+ end
100
+
101
+ body
102
+ end
103
+
104
+ def trim(value)
105
+ return nil if value.nil?
106
+
107
+ trimmed = value.to_s.strip
108
+ trimmed.empty? ? nil : trimmed
109
+ end
110
+
111
+ def assert_max_length(field, value, max)
112
+ return if value.length <= max
113
+
114
+ message = "#{field} must be at most #{max} characters"
115
+ raise ValidationError.new(message, { field => [message] })
116
+ end
117
+
118
+ def assert_enum(field, value, allowed)
119
+ return if allowed.include?(value)
120
+
121
+ message = "#{field} must be one of: #{allowed.join(', ')}"
122
+ raise ValidationError.new(message, { field => [message] })
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trigv
4
+ VERSION = '1.0.0'
5
+ end
data/lib/trigv.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'trigv/version'
4
+ require 'trigv/errors'
5
+ require 'trigv/validation'
6
+ require 'trigv/client'
7
+
8
+ module Trigv
9
+ DEFAULT_BASE_URL = Validation::DEFAULT_BASE_URL
10
+ MAX_RETRIES = Validation::MAX_RETRIES
11
+ LEVELS = Validation::LEVELS
12
+ DELIVERY_URGENCIES = Validation::DELIVERY_URGENCIES
13
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: trigv
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Trigv
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: Thin HTTP client for the Trigv workspace ingest API (events and connection
13
+ verification).
14
+ email:
15
+ - hello@trigv.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - CHANGELOG.md
21
+ - LICENSE
22
+ - README.md
23
+ - lib/trigv.rb
24
+ - lib/trigv/client.rb
25
+ - lib/trigv/errors.rb
26
+ - lib/trigv/retry.rb
27
+ - lib/trigv/validation.rb
28
+ - lib/trigv/version.rb
29
+ homepage: https://github.com/Trigv/trigv-ruby
30
+ licenses:
31
+ - MIT
32
+ metadata:
33
+ homepage_uri: https://github.com/Trigv/trigv-ruby
34
+ source_code_uri: https://github.com/Trigv/trigv-ruby
35
+ rubygems_mfa_required: 'true'
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '3.2'
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubygems_version: 4.0.11
51
+ specification_version: 4
52
+ summary: Official Trigv SDK for Ruby — send notification events to your workspace
53
+ test_files: []