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.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +137 -0
  4. data/.simplecov +46 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +33 -0
  7. data/CODE_OF_CONDUCT.md +128 -0
  8. data/CONTRIBUTING.md +165 -0
  9. data/Gemfile +43 -0
  10. data/Guardfile +34 -0
  11. data/LICENSE.txt +21 -0
  12. data/PUBLISHING_CHECKLIST.md +214 -0
  13. data/README.md +171 -0
  14. data/Rakefile +165 -0
  15. data/docs/agent_execution.md +309 -0
  16. data/docs/api_reference.md +792 -0
  17. data/docs/configuration.md +780 -0
  18. data/docs/events.md +475 -0
  19. data/docs/getting_started.md +668 -0
  20. data/docs/integration.md +262 -0
  21. data/docs/server_apps.md +621 -0
  22. data/docs/troubleshooting.md +765 -0
  23. data/lib/a2a/client/api_methods.rb +263 -0
  24. data/lib/a2a/client/auth/api_key.rb +161 -0
  25. data/lib/a2a/client/auth/interceptor.rb +288 -0
  26. data/lib/a2a/client/auth/jwt.rb +189 -0
  27. data/lib/a2a/client/auth/oauth2.rb +146 -0
  28. data/lib/a2a/client/auth.rb +137 -0
  29. data/lib/a2a/client/base.rb +316 -0
  30. data/lib/a2a/client/config.rb +210 -0
  31. data/lib/a2a/client/connection_pool.rb +233 -0
  32. data/lib/a2a/client/http_client.rb +524 -0
  33. data/lib/a2a/client/json_rpc_handler.rb +136 -0
  34. data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
  35. data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
  36. data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
  37. data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
  38. data/lib/a2a/client/middleware.rb +116 -0
  39. data/lib/a2a/client/performance_tracker.rb +60 -0
  40. data/lib/a2a/configuration/defaults.rb +34 -0
  41. data/lib/a2a/configuration/environment_loader.rb +76 -0
  42. data/lib/a2a/configuration/file_loader.rb +115 -0
  43. data/lib/a2a/configuration/inheritance.rb +101 -0
  44. data/lib/a2a/configuration/validator.rb +180 -0
  45. data/lib/a2a/configuration.rb +201 -0
  46. data/lib/a2a/errors.rb +291 -0
  47. data/lib/a2a/modules.rb +50 -0
  48. data/lib/a2a/monitoring/alerting.rb +490 -0
  49. data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
  50. data/lib/a2a/monitoring/health_endpoints.rb +204 -0
  51. data/lib/a2a/monitoring/metrics_collector.rb +438 -0
  52. data/lib/a2a/monitoring.rb +463 -0
  53. data/lib/a2a/plugin.rb +358 -0
  54. data/lib/a2a/plugin_manager.rb +159 -0
  55. data/lib/a2a/plugins/example_auth.rb +81 -0
  56. data/lib/a2a/plugins/example_middleware.rb +118 -0
  57. data/lib/a2a/plugins/example_transport.rb +76 -0
  58. data/lib/a2a/protocol/agent_card.rb +8 -0
  59. data/lib/a2a/protocol/agent_card_server.rb +584 -0
  60. data/lib/a2a/protocol/capability.rb +496 -0
  61. data/lib/a2a/protocol/json_rpc.rb +254 -0
  62. data/lib/a2a/protocol/message.rb +8 -0
  63. data/lib/a2a/protocol/task.rb +8 -0
  64. data/lib/a2a/rails/a2a_controller.rb +258 -0
  65. data/lib/a2a/rails/controller_helpers.rb +499 -0
  66. data/lib/a2a/rails/engine.rb +167 -0
  67. data/lib/a2a/rails/generators/agent_generator.rb +311 -0
  68. data/lib/a2a/rails/generators/install_generator.rb +209 -0
  69. data/lib/a2a/rails/generators/migration_generator.rb +232 -0
  70. data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
  71. data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
  72. data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
  73. data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
  74. data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
  75. data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
  76. data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
  77. data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
  78. data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
  79. data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
  80. data/lib/a2a/rails/tasks/a2a.rake +228 -0
  81. data/lib/a2a/server/a2a_methods.rb +520 -0
  82. data/lib/a2a/server/agent.rb +537 -0
  83. data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
  84. data/lib/a2a/server/agent_execution/request_context.rb +219 -0
  85. data/lib/a2a/server/apps/rack_app.rb +311 -0
  86. data/lib/a2a/server/apps/sinatra_app.rb +261 -0
  87. data/lib/a2a/server/default_request_handler.rb +350 -0
  88. data/lib/a2a/server/events/event_consumer.rb +116 -0
  89. data/lib/a2a/server/events/event_queue.rb +226 -0
  90. data/lib/a2a/server/example_agent.rb +248 -0
  91. data/lib/a2a/server/handler.rb +281 -0
  92. data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
  93. data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
  94. data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
  95. data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
  96. data/lib/a2a/server/middleware.rb +213 -0
  97. data/lib/a2a/server/push_notification_manager.rb +327 -0
  98. data/lib/a2a/server/request_handler.rb +136 -0
  99. data/lib/a2a/server/storage/base.rb +141 -0
  100. data/lib/a2a/server/storage/database.rb +266 -0
  101. data/lib/a2a/server/storage/memory.rb +274 -0
  102. data/lib/a2a/server/storage/redis.rb +320 -0
  103. data/lib/a2a/server/storage.rb +38 -0
  104. data/lib/a2a/server/task_manager.rb +534 -0
  105. data/lib/a2a/transport/grpc.rb +481 -0
  106. data/lib/a2a/transport/http.rb +415 -0
  107. data/lib/a2a/transport/sse.rb +499 -0
  108. data/lib/a2a/types/agent_card.rb +540 -0
  109. data/lib/a2a/types/artifact.rb +99 -0
  110. data/lib/a2a/types/base_model.rb +223 -0
  111. data/lib/a2a/types/events.rb +117 -0
  112. data/lib/a2a/types/message.rb +106 -0
  113. data/lib/a2a/types/part.rb +288 -0
  114. data/lib/a2a/types/push_notification.rb +139 -0
  115. data/lib/a2a/types/security.rb +167 -0
  116. data/lib/a2a/types/task.rb +154 -0
  117. data/lib/a2a/types.rb +88 -0
  118. data/lib/a2a/utils/helpers.rb +245 -0
  119. data/lib/a2a/utils/message_buffer.rb +278 -0
  120. data/lib/a2a/utils/performance.rb +247 -0
  121. data/lib/a2a/utils/rails_detection.rb +97 -0
  122. data/lib/a2a/utils/structured_logger.rb +306 -0
  123. data/lib/a2a/utils/time_helpers.rb +167 -0
  124. data/lib/a2a/utils/validation.rb +8 -0
  125. data/lib/a2a/version.rb +6 -0
  126. data/lib/a2a-rails.rb +58 -0
  127. data/lib/a2a.rb +198 -0
  128. 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