morphe_adapter_sdk 1.0.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 +7 -0
- data/lib/morphe_adapter_sdk/client.rb +171 -0
- data/lib/morphe_adapter_sdk/errors.rb +54 -0
- data/lib/morphe_adapter_sdk/hmac.rb +19 -0
- data/lib/morphe_adapter_sdk/retry.rb +33 -0
- data/lib/morphe_adapter_sdk/version.rb +26 -0
- data/lib/morphe_adapter_sdk.rb +20 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b3eeea159d98c6a1db3ad05249f184c498177a79a35f9be08c9b7841f1350411
|
|
4
|
+
data.tar.gz: dde2121058ab5dfee203bd9ee63ade7aca93a7d72915d122dc8191c963d98c1d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: aab8206c95bf83c3d9995230c70c7ac181dab1bee7348608f966e2a1e3a494261a8cb529ca09caa21551cf654b254f84205356ce5fa890bd86624ccb22eacb6f
|
|
7
|
+
data.tar.gz: d33bb18d879de995fdcae194b492042d6119c3350835f7375b8dd97c91daf12015126ca9e4202257d5978822f1c9a79ab8579cda0c7d1f7b0aba275735055f0f
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module MorpheAdapterSdk
|
|
8
|
+
# Synchronous client for emitting events to the Morphe Control Plane.
|
|
9
|
+
#
|
|
10
|
+
# Supports:
|
|
11
|
+
# - Optional pre-emit JSON Schema validation (+validate_schema: true+)
|
|
12
|
+
# - HMAC-SHA256 request signing (when +hmac_secret:+ is provided)
|
|
13
|
+
# - Exponential backoff retry on 5xx / network errors
|
|
14
|
+
# - Structured JSON:API error surface (ApiError, SchemaValidationError, NetworkError)
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# client = MorpheAdapterSdk::Client.new(token: ENV["MORPHE_TOKEN"])
|
|
18
|
+
# result = client.emit(
|
|
19
|
+
# event_type: "dataops.v1.sync_failed",
|
|
20
|
+
# system_id: "11111111-1111-1111-1111-111111111111",
|
|
21
|
+
# payload: { records_failed: 42 },
|
|
22
|
+
# idempotency_key: "req-abc-123"
|
|
23
|
+
# )
|
|
24
|
+
# puts result.id
|
|
25
|
+
class Client
|
|
26
|
+
DEFAULT_MAX_RETRIES = 3
|
|
27
|
+
DEFAULT_BASE_DELAY = 0.5 # seconds
|
|
28
|
+
|
|
29
|
+
# @!attribute [r] endpoint
|
|
30
|
+
# @return [String] resolved Control Plane base URL
|
|
31
|
+
attr_reader :endpoint
|
|
32
|
+
|
|
33
|
+
# @param token [String] JWT Bearer token
|
|
34
|
+
# @param endpoint [String, nil] override base URL (dev/self-hosted only)
|
|
35
|
+
# @param hmac_secret [String, nil] HMAC-SHA256 shared secret
|
|
36
|
+
# @param max_retries [Integer] default: 3
|
|
37
|
+
# @param base_delay [Float] default: 0.5 s
|
|
38
|
+
# @param validate_schema [Boolean] validate payload locally before emitting
|
|
39
|
+
def initialize(
|
|
40
|
+
token:,
|
|
41
|
+
endpoint: nil,
|
|
42
|
+
hmac_secret: nil,
|
|
43
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
|
44
|
+
base_delay: DEFAULT_BASE_DELAY,
|
|
45
|
+
validate_schema: false
|
|
46
|
+
)
|
|
47
|
+
raw = endpoint || ENV["MORPHE_ENDPOINT"] || DEFAULT_ENDPOINT
|
|
48
|
+
@endpoint = raw.chomp("/")
|
|
49
|
+
@token = token
|
|
50
|
+
@hmac_secret = hmac_secret
|
|
51
|
+
@max_retries = max_retries
|
|
52
|
+
@base_delay = base_delay
|
|
53
|
+
@validate_schema = validate_schema
|
|
54
|
+
@schema_cache = {}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Emit a single event to the Control Plane.
|
|
58
|
+
#
|
|
59
|
+
# If +validate_schema+ was set at construction, the payload is validated
|
|
60
|
+
# against the registered schema before any HTTP request is made.
|
|
61
|
+
# Raises +SchemaValidationError+ on failure.
|
|
62
|
+
#
|
|
63
|
+
# Retries on 5xx responses and network failures with exponential backoff.
|
|
64
|
+
# Raises +ApiError+ on non-retryable HTTP errors (4xx).
|
|
65
|
+
# Raises +NetworkError+ when the Control Plane cannot be reached.
|
|
66
|
+
#
|
|
67
|
+
# @param event [Hash] event payload (symbol or string keys)
|
|
68
|
+
# @return [EmitResult]
|
|
69
|
+
def emit(event)
|
|
70
|
+
event = stringify_keys(event)
|
|
71
|
+
|
|
72
|
+
validate!(event) if @validate_schema
|
|
73
|
+
|
|
74
|
+
MorpheAdapterSdk::Retry.call(
|
|
75
|
+
max_retries: @max_retries,
|
|
76
|
+
base_delay: @base_delay,
|
|
77
|
+
should_retry: ->(err) {
|
|
78
|
+
err.is_a?(NetworkError) ||
|
|
79
|
+
(err.is_a?(ApiError) && err.status >= 500)
|
|
80
|
+
}
|
|
81
|
+
) { do_request(event) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
EmitResult = Struct.new(:id, :received_at, :schema_valid, :schema_validation_errors, keyword_init: true)
|
|
87
|
+
|
|
88
|
+
def do_request(event)
|
|
89
|
+
body = JSON.generate(event)
|
|
90
|
+
version = MorpheAdapterSdk.extract_api_version(event["event_type"])
|
|
91
|
+
uri = URI.parse("#{@endpoint}/api/#{version}/events")
|
|
92
|
+
|
|
93
|
+
headers = {
|
|
94
|
+
"Content-Type" => "application/json",
|
|
95
|
+
"Authorization" => "Bearer #{@token}"
|
|
96
|
+
}
|
|
97
|
+
headers["X-Morphe-Signature"] = MorpheAdapterSdk::Hmac.sign(body, @hmac_secret) if @hmac_secret
|
|
98
|
+
|
|
99
|
+
response = http_post(uri, body, headers)
|
|
100
|
+
|
|
101
|
+
if response.code.to_i == 202
|
|
102
|
+
data = JSON.parse(response.body)
|
|
103
|
+
attrs = data.dig("data", "attributes")
|
|
104
|
+
return EmitResult.new(
|
|
105
|
+
id: data.dig("data", "id"),
|
|
106
|
+
received_at: attrs["received_at"],
|
|
107
|
+
schema_valid: attrs["schema_valid"],
|
|
108
|
+
schema_validation_errors: attrs["schema_validation_errors"] || []
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
errors = parse_errors(response)
|
|
113
|
+
raise ApiError.new(response.code.to_i, errors)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def http_post(uri, body, headers)
|
|
117
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
|
|
118
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
|
119
|
+
req.body = body
|
|
120
|
+
http.request(req)
|
|
121
|
+
end
|
|
122
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout => e
|
|
123
|
+
raise NetworkError.new("Failed to reach the Control Plane", e)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parse_errors(response)
|
|
127
|
+
body = JSON.parse(response.body)
|
|
128
|
+
body["errors"] || []
|
|
129
|
+
rescue JSON::ParserError
|
|
130
|
+
[ { "code" => "parse_error", "title" => "Could not parse error response" } ]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def validate!(event)
|
|
134
|
+
require "json-schema"
|
|
135
|
+
|
|
136
|
+
event_type = event["event_type"]
|
|
137
|
+
schema = @schema_cache[event_type] ||= fetch_schema(event_type)
|
|
138
|
+
payload = event["payload"] || {}
|
|
139
|
+
|
|
140
|
+
raw_errors = JSON::Validator.fully_validate(schema, payload)
|
|
141
|
+
return if raw_errors.empty?
|
|
142
|
+
|
|
143
|
+
failures = raw_errors.map { |msg| { field: "/", message: msg } }
|
|
144
|
+
raise SchemaValidationError.new(failures)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def fetch_schema(event_type)
|
|
148
|
+
version = MorpheAdapterSdk.extract_api_version(event_type)
|
|
149
|
+
uri = URI.parse("#{@endpoint}/api/#{version}/schemas/#{URI.encode_www_form_component(event_type)}")
|
|
150
|
+
headers = { "Authorization" => "Bearer #{@token}" }
|
|
151
|
+
|
|
152
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10, read_timeout: 30) do |http|
|
|
153
|
+
http.request(Net::HTTP::Get.new(uri.request_uri, headers))
|
|
154
|
+
end
|
|
155
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout => e
|
|
156
|
+
raise NetworkError.new("Failed to fetch schema for #{event_type}", e)
|
|
157
|
+
else
|
|
158
|
+
unless (200..299).cover?(response.code.to_i)
|
|
159
|
+
errors = parse_errors(response)
|
|
160
|
+
raise ApiError.new(response.code.to_i, errors.empty? ? [ { "code" => "schema_not_found", "title" => "No schema registered for \"#{event_type}\"" } ] : errors)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
data = JSON.parse(response.body)
|
|
164
|
+
data.dig("data", "attributes", "schema")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def stringify_keys(hash)
|
|
168
|
+
hash.transform_keys(&:to_s)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MorpheAdapterSdk
|
|
4
|
+
# Base class for all Morphe SDK errors.
|
|
5
|
+
# Callers can rescue MorpheAdapterSdk::Error to catch any SDK error.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Raised when the Control Plane returns a non-2xx response.
|
|
9
|
+
#
|
|
10
|
+
# Exposes the first JSON:API error's +code+, +title+, and +detail+
|
|
11
|
+
# fields as top-level attributes for easy exception handling, plus the
|
|
12
|
+
# full +errors+ array for callers that want all details.
|
|
13
|
+
class ApiError < Error
|
|
14
|
+
attr_reader :status, :code, :title, :detail, :errors
|
|
15
|
+
|
|
16
|
+
# @param status [Integer] HTTP status code
|
|
17
|
+
# @param errors [Array<Hash>] JSON:API errors array
|
|
18
|
+
def initialize(status, errors = [])
|
|
19
|
+
first = errors.first || {}
|
|
20
|
+
@status = status
|
|
21
|
+
@code = first["code"] || "unknown_error"
|
|
22
|
+
@title = first["title"] || "Unknown error"
|
|
23
|
+
@detail = first["detail"]
|
|
24
|
+
@errors = errors
|
|
25
|
+
super("#{@status} #{@title}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Raised when pre-emit schema validation fails.
|
|
30
|
+
# No HTTP request is made — the payload was rejected locally.
|
|
31
|
+
class SchemaValidationError < Error
|
|
32
|
+
attr_reader :failures
|
|
33
|
+
|
|
34
|
+
# @param failures [Array<Hash>] array of {field:, message:} hashes
|
|
35
|
+
def initialize(failures)
|
|
36
|
+
@failures = failures
|
|
37
|
+
parts = failures.map { |f| "#{f[:field]} #{f[:message]}" }
|
|
38
|
+
super("Schema validation failed: #{parts.join('; ')}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Raised when a network-level failure prevents reaching the Control Plane.
|
|
43
|
+
# Retries are already exhausted before this is raised.
|
|
44
|
+
class NetworkError < Error
|
|
45
|
+
attr_reader :original_error
|
|
46
|
+
|
|
47
|
+
# @param message [String]
|
|
48
|
+
# @param original_error [Exception]
|
|
49
|
+
def initialize(message, original_error)
|
|
50
|
+
@original_error = original_error
|
|
51
|
+
super(message)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module MorpheAdapterSdk
|
|
6
|
+
# HMAC-SHA256 signing utility.
|
|
7
|
+
#
|
|
8
|
+
# Produces the value expected by the X-Morphe-Signature header.
|
|
9
|
+
# The result is identical to the TypeScript and Python SDK sign_body
|
|
10
|
+
# implementations when both receive the same UTF-8 encoded inputs.
|
|
11
|
+
module Hmac
|
|
12
|
+
# @param body [String] raw JSON request body (UTF-8)
|
|
13
|
+
# @param secret [String] HMAC shared secret from the API credential
|
|
14
|
+
# @return [String] 64-character lowercase hex digest
|
|
15
|
+
def self.sign(body, secret)
|
|
16
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret, body)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MorpheAdapterSdk
|
|
4
|
+
# Exponential backoff retry utility.
|
|
5
|
+
#
|
|
6
|
+
# - Configurable +max_retries+ and +base_delay+.
|
|
7
|
+
# - Jitter in [0.5, 1.5] to spread retries across clients.
|
|
8
|
+
# - Injectable +sleep_fn+ for test isolation (avoids stubbing Kernel#sleep).
|
|
9
|
+
module Retry
|
|
10
|
+
# @param max_retries [Integer] maximum retry attempts (0 = no retries)
|
|
11
|
+
# @param base_delay [Float] base delay in seconds before first retry
|
|
12
|
+
# @param should_retry [Proc] predicate: returns true if the error is transient
|
|
13
|
+
# @param sleep_fn [Proc] injectable sleep; defaults to Kernel.method(:sleep)
|
|
14
|
+
# @yield zero-argument block to execute and retry
|
|
15
|
+
# @return the return value of the block on success
|
|
16
|
+
# @raise the last exception after all retries exhausted
|
|
17
|
+
def self.call(max_retries:, base_delay:, should_retry:, sleep_fn: nil, &block)
|
|
18
|
+
sleep_fn ||= method(:sleep)
|
|
19
|
+
attempt = 0
|
|
20
|
+
|
|
21
|
+
loop do
|
|
22
|
+
return yield
|
|
23
|
+
rescue => err
|
|
24
|
+
raise if attempt >= max_retries || !should_retry.call(err)
|
|
25
|
+
|
|
26
|
+
jitter = 0.5 + rand
|
|
27
|
+
delay = base_delay * (2**attempt) * jitter
|
|
28
|
+
sleep_fn.call(delay)
|
|
29
|
+
attempt += 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MorpheAdapterSdk
|
|
4
|
+
VERSION = "1.0.0"
|
|
5
|
+
|
|
6
|
+
# Default production endpoint for the Morphe Control Plane.
|
|
7
|
+
# Override via the MORPHE_ENDPOINT environment variable for development
|
|
8
|
+
# or self-hosted deployments.
|
|
9
|
+
DEFAULT_ENDPOINT = "https://api.morphe.io"
|
|
10
|
+
|
|
11
|
+
# Extract the API version segment from an event_type string.
|
|
12
|
+
#
|
|
13
|
+
# The version is the first dot-delimited part matching /^v\d+$/.
|
|
14
|
+
# Falls back to "v1" when no version segment is present.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# MorpheAdapterSdk.extract_api_version("dataops.v1.sync_failed") # => "v1"
|
|
18
|
+
# MorpheAdapterSdk.extract_api_version("dataops.v2.sync_failed") # => "v2"
|
|
19
|
+
# MorpheAdapterSdk.extract_api_version("dataops.sync_failed") # => "v1"
|
|
20
|
+
#
|
|
21
|
+
# @param event_type [String]
|
|
22
|
+
# @return [String]
|
|
23
|
+
def self.extract_api_version(event_type)
|
|
24
|
+
event_type.split(".").find { |part| part.match?(/\Av\d+\z/) } || "v1"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "morphe_adapter_sdk/version"
|
|
4
|
+
require_relative "morphe_adapter_sdk/errors"
|
|
5
|
+
require_relative "morphe_adapter_sdk/hmac"
|
|
6
|
+
require_relative "morphe_adapter_sdk/retry"
|
|
7
|
+
require_relative "morphe_adapter_sdk/client"
|
|
8
|
+
|
|
9
|
+
# MorpheAdapterSdk — Ruby adapter for emitting governance events to the
|
|
10
|
+
# Morphe Control Plane.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# client = MorpheAdapterSdk::Client.new(token: ENV["MORPHE_TOKEN"])
|
|
14
|
+
# result = client.emit(
|
|
15
|
+
# event_type: "dataops.v1.sync_failed",
|
|
16
|
+
# system_id: "11111111-1111-1111-1111-111111111111",
|
|
17
|
+
# payload: { records_failed: 42 }
|
|
18
|
+
# )
|
|
19
|
+
module MorpheAdapterSdk
|
|
20
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: morphe_adapter_sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Morphe
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-04 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: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.13'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.13'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: webmock
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.23'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.23'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rack
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3'
|
|
69
|
+
description:
|
|
70
|
+
email:
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- lib/morphe_adapter_sdk.rb
|
|
76
|
+
- lib/morphe_adapter_sdk/client.rb
|
|
77
|
+
- lib/morphe_adapter_sdk/errors.rb
|
|
78
|
+
- lib/morphe_adapter_sdk/hmac.rb
|
|
79
|
+
- lib/morphe_adapter_sdk/retry.rb
|
|
80
|
+
- lib/morphe_adapter_sdk/version.rb
|
|
81
|
+
homepage:
|
|
82
|
+
licenses: []
|
|
83
|
+
metadata:
|
|
84
|
+
allowed_push_host: https://rubygems.org
|
|
85
|
+
post_install_message:
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '3.1'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.5.22
|
|
101
|
+
signing_key:
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: Ruby adapter SDK for emitting events to the Morphe Control Plane
|
|
104
|
+
test_files: []
|