a2a-ruby 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module A2A
|
4
|
+
module Types
|
5
|
+
##
|
6
|
+
# Represents a task in the A2A protocol
|
7
|
+
#
|
8
|
+
# A task represents a unit of work that can be executed by an agent.
|
9
|
+
# It includes status information, artifacts, message history, and metadata.
|
10
|
+
#
|
11
|
+
class Task < A2A::Types::BaseModel
|
12
|
+
attr_reader :id, :context_id, :kind, :status, :artifacts, :history, :metadata
|
13
|
+
|
14
|
+
##
|
15
|
+
# Initialize a new task
|
16
|
+
#
|
17
|
+
# @param id [String] Unique task identifier
|
18
|
+
# @param context_id [String] Context identifier for grouping related tasks
|
19
|
+
# @param status [TaskStatus, Hash] Current task status
|
20
|
+
# @param kind [String] Task kind (always "task")
|
21
|
+
# @param artifacts [Array<Artifact>, nil] Task artifacts
|
22
|
+
# @param history [Array<Message>, nil] Message history
|
23
|
+
# @param metadata [Hash, nil] Additional metadata
|
24
|
+
def initialize(id:, context_id:, status:, kind: KIND_TASK, artifacts: nil, history: nil, metadata: nil)
|
25
|
+
@id = id
|
26
|
+
@context_id = context_id
|
27
|
+
@kind = kind
|
28
|
+
@status = status.is_a?(TaskStatus) ? status : TaskStatus.from_h(status)
|
29
|
+
@artifacts = artifacts&.map { |a| a.is_a?(Artifact) ? a : Artifact.from_h(a) }
|
30
|
+
@history = history&.map { |m| m.is_a?(Message) ? m : Message.from_h(m) }
|
31
|
+
@metadata = metadata
|
32
|
+
|
33
|
+
validate!
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Add an artifact to the task
|
38
|
+
#
|
39
|
+
# @param artifact [Artifact] The artifact to add
|
40
|
+
def add_artifact(artifact)
|
41
|
+
@artifacts ||= []
|
42
|
+
@artifacts << artifact
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Add a message to the history
|
47
|
+
#
|
48
|
+
# @param message [Message] The message to add
|
49
|
+
def add_message(message)
|
50
|
+
@history ||= []
|
51
|
+
@history << message
|
52
|
+
end
|
53
|
+
|
54
|
+
##
|
55
|
+
# Update the task status
|
56
|
+
#
|
57
|
+
# @param new_status [TaskStatus, Hash] The new status
|
58
|
+
def update_status(new_status)
|
59
|
+
@status = new_status.is_a?(TaskStatus) ? new_status : TaskStatus.from_h(new_status)
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Check if the task is in a terminal state
|
64
|
+
#
|
65
|
+
# @return [Boolean] True if the task is completed, canceled, failed, or rejected
|
66
|
+
def terminal?
|
67
|
+
%w[completed canceled failed rejected].include?(@status.state)
|
68
|
+
end
|
69
|
+
|
70
|
+
##
|
71
|
+
# Check if the task can be canceled
|
72
|
+
#
|
73
|
+
# @return [Boolean] True if the task can be canceled
|
74
|
+
def cancelable?
|
75
|
+
%w[submitted working input-required].include?(@status.state)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def validate!
|
81
|
+
validate_required(:id, :context_id, :status, :kind)
|
82
|
+
validate_inclusion(:kind, [KIND_TASK])
|
83
|
+
validate_type(:status, TaskStatus)
|
84
|
+
validate_array_type(:artifacts, Artifact) if @artifacts
|
85
|
+
validate_array_type(:history, Message) if @history
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# Represents the status of a task
|
91
|
+
#
|
92
|
+
class TaskStatus < A2A::Types::BaseModel
|
93
|
+
attr_reader :state, :message, :progress, :result, :error, :updated_at
|
94
|
+
|
95
|
+
##
|
96
|
+
# Initialize a new task status
|
97
|
+
#
|
98
|
+
# @param state [String] The current state
|
99
|
+
# @param message [String, nil] Optional status message
|
100
|
+
# @param progress [Float, nil] Progress percentage (0.0 to 1.0)
|
101
|
+
# @param result [Object, nil] Task result (for completed tasks)
|
102
|
+
# @param error [Hash, nil] Error information (for failed tasks)
|
103
|
+
# @param updated_at [String, nil] ISO 8601 timestamp of last update
|
104
|
+
def initialize(state:, message: nil, progress: nil, result: nil, error: nil, updated_at: nil)
|
105
|
+
@state = state
|
106
|
+
@message = message
|
107
|
+
@progress = progress
|
108
|
+
@result = result
|
109
|
+
@error = error
|
110
|
+
@updated_at = updated_at || Time.now.utc.iso8601
|
111
|
+
|
112
|
+
validate!
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Check if the status indicates success
|
117
|
+
#
|
118
|
+
# @return [Boolean] True if the task completed successfully
|
119
|
+
def success?
|
120
|
+
@state == TASK_STATE_COMPLETED && @error.nil?
|
121
|
+
end
|
122
|
+
|
123
|
+
##
|
124
|
+
# Check if the status indicates failure
|
125
|
+
#
|
126
|
+
# @return [Boolean] True if the task failed
|
127
|
+
def failure?
|
128
|
+
@state == TASK_STATE_FAILED || !@error.nil?
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Check if the task is still active
|
133
|
+
#
|
134
|
+
# @return [Boolean] True if the task is still being processed
|
135
|
+
def active?
|
136
|
+
%w[submitted working input-required].include?(@state)
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def validate!
|
142
|
+
validate_required(:state, :updated_at)
|
143
|
+
validate_inclusion(:state, VALID_TASK_STATES)
|
144
|
+
|
145
|
+
return unless @progress
|
146
|
+
|
147
|
+
validate_type(:progress, Numeric)
|
148
|
+
return if @progress.between?(0.0, 1.0)
|
149
|
+
|
150
|
+
raise ArgumentError, "progress must be between 0.0 and 1.0"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/a2a/types.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "types/base_model"
|
4
|
+
require_relative "types/agent_card"
|
5
|
+
require_relative "types/message"
|
6
|
+
require_relative "types/task"
|
7
|
+
require_relative "types/part"
|
8
|
+
require_relative "types/artifact"
|
9
|
+
require_relative "types/events"
|
10
|
+
require_relative "types/push_notification"
|
11
|
+
require_relative "types/security"
|
12
|
+
|
13
|
+
##
|
14
|
+
# Type definitions for the A2A protocol
|
15
|
+
#
|
16
|
+
# This module contains all the data types used in the A2A protocol,
|
17
|
+
# including messages, tasks, agent cards, and various supporting types.
|
18
|
+
#
|
19
|
+
module A2A
|
20
|
+
module Types
|
21
|
+
# Transport protocol constants
|
22
|
+
TRANSPORT_JSONRPC = "JSONRPC"
|
23
|
+
TRANSPORT_GRPC = "GRPC"
|
24
|
+
TRANSPORT_HTTP_JSON = "HTTP+JSON"
|
25
|
+
|
26
|
+
# Valid transport protocols
|
27
|
+
VALID_TRANSPORTS = [TRANSPORT_JSONRPC, TRANSPORT_GRPC, TRANSPORT_HTTP_JSON].freeze
|
28
|
+
|
29
|
+
# Message roles
|
30
|
+
ROLE_USER = "user"
|
31
|
+
ROLE_AGENT = "agent"
|
32
|
+
|
33
|
+
# Valid message roles
|
34
|
+
VALID_ROLES = [ROLE_USER, ROLE_AGENT].freeze
|
35
|
+
|
36
|
+
# Task states
|
37
|
+
TASK_STATE_SUBMITTED = "submitted"
|
38
|
+
TASK_STATE_WORKING = "working"
|
39
|
+
TASK_STATE_INPUT_REQUIRED = "input-required"
|
40
|
+
TASK_STATE_COMPLETED = "completed"
|
41
|
+
TASK_STATE_CANCELED = "canceled"
|
42
|
+
TASK_STATE_FAILED = "failed"
|
43
|
+
TASK_STATE_REJECTED = "rejected"
|
44
|
+
TASK_STATE_AUTH_REQUIRED = "auth-required"
|
45
|
+
TASK_STATE_UNKNOWN = "unknown"
|
46
|
+
|
47
|
+
# Valid task states
|
48
|
+
VALID_TASK_STATES = [
|
49
|
+
TASK_STATE_SUBMITTED,
|
50
|
+
TASK_STATE_WORKING,
|
51
|
+
TASK_STATE_INPUT_REQUIRED,
|
52
|
+
TASK_STATE_COMPLETED,
|
53
|
+
TASK_STATE_CANCELED,
|
54
|
+
TASK_STATE_FAILED,
|
55
|
+
TASK_STATE_REJECTED,
|
56
|
+
TASK_STATE_AUTH_REQUIRED,
|
57
|
+
TASK_STATE_UNKNOWN
|
58
|
+
].freeze
|
59
|
+
|
60
|
+
# Part kinds
|
61
|
+
PART_KIND_TEXT = "text"
|
62
|
+
PART_KIND_FILE = "file"
|
63
|
+
PART_KIND_DATA = "data"
|
64
|
+
|
65
|
+
# Valid part kinds
|
66
|
+
VALID_PART_KINDS = [PART_KIND_TEXT, PART_KIND_FILE, PART_KIND_DATA].freeze
|
67
|
+
|
68
|
+
# Object kinds
|
69
|
+
KIND_MESSAGE = "message"
|
70
|
+
KIND_TASK = "task"
|
71
|
+
|
72
|
+
# Security scheme types
|
73
|
+
SECURITY_TYPE_API_KEY = "apiKey"
|
74
|
+
SECURITY_TYPE_HTTP = "http"
|
75
|
+
SECURITY_TYPE_OAUTH2 = "oauth2"
|
76
|
+
SECURITY_TYPE_OPENID_CONNECT = "openIdConnect"
|
77
|
+
SECURITY_TYPE_MUTUAL_TLS = "mutualTLS"
|
78
|
+
|
79
|
+
# Valid security scheme types
|
80
|
+
VALID_SECURITY_TYPES = [
|
81
|
+
SECURITY_TYPE_API_KEY,
|
82
|
+
SECURITY_TYPE_HTTP,
|
83
|
+
SECURITY_TYPE_OAUTH2,
|
84
|
+
SECURITY_TYPE_OPENID_CONNECT,
|
85
|
+
SECURITY_TYPE_MUTUAL_TLS
|
86
|
+
].freeze
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
require "digest"
|
5
|
+
require "base64"
|
6
|
+
|
7
|
+
##
|
8
|
+
# Common utility helper methods
|
9
|
+
#
|
10
|
+
# Provides various utility methods for UUID generation, string manipulation,
|
11
|
+
# encoding/decoding, and other common operations used throughout the A2A gem.
|
12
|
+
#
|
13
|
+
module A2A
|
14
|
+
module Utils
|
15
|
+
module Helpers
|
16
|
+
class << self
|
17
|
+
##
|
18
|
+
# Generate a UUID
|
19
|
+
#
|
20
|
+
# @return [String] A new UUID string
|
21
|
+
def generate_uuid
|
22
|
+
SecureRandom.uuid
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
# Generate a random hex string
|
27
|
+
#
|
28
|
+
# @param length [Integer] Length of the hex string (default: 16)
|
29
|
+
# @return [String] Random hex string
|
30
|
+
def generate_hex(length = 16)
|
31
|
+
SecureRandom.hex(length)
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Generate a secure random token
|
36
|
+
#
|
37
|
+
# @param length [Integer] Length of the token (default: 32)
|
38
|
+
# @return [String] Base64-encoded random token
|
39
|
+
def generate_token(length = 32)
|
40
|
+
Base64.urlsafe_encode64(SecureRandom.random_bytes(length), padding: false)
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Generate a hash of a string
|
45
|
+
#
|
46
|
+
# @param string [String] String to hash
|
47
|
+
# @param algorithm [Symbol] Hash algorithm (:sha256, :sha1, :md5)
|
48
|
+
# @return [String] Hex-encoded hash
|
49
|
+
def hash_string(string, algorithm: :sha256)
|
50
|
+
case algorithm
|
51
|
+
when :sha256
|
52
|
+
Digest::SHA256.hexdigest(string)
|
53
|
+
when :sha1
|
54
|
+
Digest::SHA1.hexdigest(string)
|
55
|
+
when :md5
|
56
|
+
Digest::MD5.hexdigest(string)
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Unsupported hash algorithm: #{algorithm}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Safely parse JSON with error handling
|
64
|
+
#
|
65
|
+
# @param json_string [String] JSON string to parse
|
66
|
+
# @param default [Object] Default value if parsing fails
|
67
|
+
# @return [Object] Parsed JSON or default value
|
68
|
+
def safe_json_parse(json_string, default: nil)
|
69
|
+
JSON.parse(json_string)
|
70
|
+
rescue JSON::ParserError
|
71
|
+
default
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Deep merge two hashes
|
76
|
+
#
|
77
|
+
# @param hash1 [Hash] First hash
|
78
|
+
# @param hash2 [Hash] Second hash
|
79
|
+
# @return [Hash] Merged hash
|
80
|
+
def deep_merge(hash1, hash2)
|
81
|
+
hash1.merge(hash2) do |_key, old_val, new_val|
|
82
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
83
|
+
deep_merge(old_val, new_val)
|
84
|
+
else
|
85
|
+
new_val
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Convert string to snake_case
|
92
|
+
#
|
93
|
+
# @param string [String] String to convert
|
94
|
+
# @return [String] Snake_case string
|
95
|
+
def snake_case(string)
|
96
|
+
string
|
97
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
98
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
99
|
+
.downcase
|
100
|
+
end
|
101
|
+
|
102
|
+
##
|
103
|
+
# Convert string to camelCase
|
104
|
+
#
|
105
|
+
# @param string [String] String to convert
|
106
|
+
# @param first_letter_uppercase [Boolean] Whether first letter should be uppercase
|
107
|
+
# @return [String] CamelCase string
|
108
|
+
def camel_case(string, first_letter_uppercase: false)
|
109
|
+
parts = string.split(/[_\-\s]+/)
|
110
|
+
result = parts.first.downcase
|
111
|
+
result += parts[1..].map(&:capitalize).join if parts.length > 1
|
112
|
+
|
113
|
+
first_letter_uppercase ? result.capitalize : result
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Truncate string to specified length
|
118
|
+
#
|
119
|
+
# @param string [String] String to truncate
|
120
|
+
# @param length [Integer] Maximum length
|
121
|
+
# @param suffix [String] Suffix to add if truncated
|
122
|
+
# @return [String] Truncated string
|
123
|
+
def truncate(string, length:, suffix: "...")
|
124
|
+
return string if string.length <= length
|
125
|
+
|
126
|
+
truncated_length = length - suffix.length
|
127
|
+
return suffix if truncated_length <= 0
|
128
|
+
|
129
|
+
string[0...truncated_length] + suffix
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Sanitize string for safe usage
|
134
|
+
#
|
135
|
+
# @param string [String] String to sanitize
|
136
|
+
# @param allowed_chars [Regexp] Allowed characters pattern
|
137
|
+
# @return [String] Sanitized string
|
138
|
+
def sanitize_string(string, allowed_chars: /[a-zA-Z0-9_\-.]/)
|
139
|
+
string.gsub(/[^#{allowed_chars.source}]/, "_")
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Check if string is blank (nil, empty, or whitespace only)
|
144
|
+
#
|
145
|
+
# @param string [String, nil] String to check
|
146
|
+
# @return [Boolean] True if blank
|
147
|
+
def blank?(string)
|
148
|
+
string.nil? || string.strip.empty?
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Check if string is present (not blank)
|
153
|
+
#
|
154
|
+
# @param string [String, nil] String to check
|
155
|
+
# @return [Boolean] True if present
|
156
|
+
def present?(string)
|
157
|
+
!blank?(string)
|
158
|
+
end
|
159
|
+
|
160
|
+
##
|
161
|
+
# Retry a block with exponential backoff
|
162
|
+
#
|
163
|
+
# @param max_attempts [Integer] Maximum number of attempts
|
164
|
+
# @param base_delay [Float] Base delay in seconds
|
165
|
+
# @param max_delay [Float] Maximum delay in seconds
|
166
|
+
# @param backoff_factor [Float] Backoff multiplier
|
167
|
+
# @yield Block to retry
|
168
|
+
# @return [Object] Block result
|
169
|
+
def retry_with_backoff(max_attempts: 3, base_delay: 1.0, max_delay: 60.0, backoff_factor: 2.0)
|
170
|
+
attempt = 1
|
171
|
+
|
172
|
+
begin
|
173
|
+
yield
|
174
|
+
rescue StandardError => e
|
175
|
+
raise e unless attempt < max_attempts
|
176
|
+
|
177
|
+
delay = [base_delay * (backoff_factor**(attempt - 1)), max_delay].min
|
178
|
+
sleep(delay)
|
179
|
+
attempt += 1
|
180
|
+
retry
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Measure execution time of a block
|
186
|
+
#
|
187
|
+
# @yield Block to measure
|
188
|
+
# @return [Hash] Result with :result and :duration keys
|
189
|
+
def measure_execution_time
|
190
|
+
start_time = Time.now
|
191
|
+
result = yield
|
192
|
+
end_time = Time.now
|
193
|
+
|
194
|
+
{
|
195
|
+
result: result,
|
196
|
+
duration: end_time - start_time
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
##
|
201
|
+
# Format bytes in human-readable format
|
202
|
+
#
|
203
|
+
# @param bytes [Integer] Number of bytes
|
204
|
+
# @return [String] Formatted string
|
205
|
+
def format_bytes(bytes)
|
206
|
+
return "0 B" if bytes.zero?
|
207
|
+
|
208
|
+
units = %w[B KB MB GB TB PB]
|
209
|
+
exp = (Math.log(bytes.abs) / Math.log(1024)).floor
|
210
|
+
exp = [exp, units.length - 1].min
|
211
|
+
|
212
|
+
"#{(bytes / (1024.0**exp)).round(2)} #{units[exp]}"
|
213
|
+
end
|
214
|
+
|
215
|
+
##
|
216
|
+
# Validate email format
|
217
|
+
#
|
218
|
+
# @param email [String] Email to validate
|
219
|
+
# @return [Boolean] True if valid email format
|
220
|
+
def valid_email?(email)
|
221
|
+
return false if blank?(email)
|
222
|
+
|
223
|
+
# Simple email validation regex
|
224
|
+
email.match?(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/)
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Validate URL format
|
229
|
+
#
|
230
|
+
# @param url [String] URL to validate
|
231
|
+
# @return [Boolean] True if valid URL format
|
232
|
+
def valid_url?(url)
|
233
|
+
return false if blank?(url)
|
234
|
+
|
235
|
+
begin
|
236
|
+
uri = URI.parse(url)
|
237
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
238
|
+
rescue URI::InvalidURIError
|
239
|
+
false
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|