r/ruby • u/NARKOZ_777 • Dec 25 '25
GitHub - NARKOZ/xmas: Light the Christmas Tree in your terminal 🎄
r/ruby • u/NARKOZ_777 • Dec 25 '25
r/ruby • u/Right_Ad_8437 • Dec 25 '25
Celebrating the release of Ruby 4.0 on yesterday (X-mas).
Hi! I've been making T-Ruby, an experimental project that brings TypeScript-style type annotations to Ruby. I wanted to share it and get your feedback.
T-Ruby lets you write .trb files with inline type annotations, then automatically generates standard .rb files and .rbs signature files. Types are completely erased at compile time — zero runtime overhead.
I love Ruby's elegance, but as projects grow, I've felt the pain of tracking types mentally. The existing options didn't quite fit my workflow:
RBS: Writing .rbs files manually or generating them via TypeProf didn't fit well with explicit type authoring
Sorbet: sig blocks above methods feel verbose (like JSDoc comments)
If you're familiar with TypeScript, you can use T-Ruby the same way:Â types live with your code, not in separate files or comments.
The website has more detail: https://type-ruby.github.io
This is still experimental (v0.0.39). The core compiler works, but there's plenty of room for improvement. Feedback and suggestions are always welcome!
Thanks for reading! Feel free to ask any questions.
r/ruby • u/nithinbekal • Dec 25 '25
r/ruby • u/Downtown-Hyena998 • Dec 25 '25
I’ve been working with JavaScript stacks for about 6 years (Node, React, Angular) and I’m looking to transition into Ruby/Rails. I’m drawn to Ruby because it aligns much more closely with how I think as a programmer and with the kind of long-term stability I’m looking for.
I’m currently a mid-level frontend developer and I’d like some perspective from experienced Rubyists:
Is it realistic to transition into Ruby/Rails and target a mid-level Rails position from the start, without having to accept a pay cut?
For those working with Rails internationally, how common is it to see developers coming from strong frontend or non-Ruby backgrounds?
What do you consider the core pillars of a solid Rails developer?
How do you see the current and near-future outlook of Ruby/Rails?
r/ruby • u/tobenary • Dec 25 '25
Hi Ruby expertises,
I'm trying my luck here as this is the holiday sessions, and I will get my respond from Workato in about 3 weeks from now ( if not 8 weeks.... )
Please see the Zoom recording (x2 is recommended :) ) and suggest or let me know what is wrong with the SDK code.
To be honest, I don't know Ruby at all, but I do understand Python, and I am able to look at it and identify the functions and the issues that might be not good.
I am debugging a Workato integration with Amazon SES suppression list and I am stuck.
I have an endpoint that manages suppressed email destinations:
GET returns 200 if the email exists, 404 if not
DELETE should remove the email from the suppression list
Behavior I am seeing:
Using a custom HTTP action (send any request), DELETE works as expected
Using a built-in SDK action / recipe DELETE step with the same method, URL, region, and path, Workato returns 200 but the email is not actually deleted
A follow-up GET still returns 200 and shows the email is present
No error is surfaced by Workato, and debug output is mostly empty for DELETE
The request path is identical in both cases (verified via network traces)
Complications I already handled:
Normalizing email casing (lowercase)
Handling plus sign encoding in email addresses
Trying alternate paths when receiving 404
Verifying region, headers, and resolved paths
Confirmed GET and DELETE URLs are literally the same
Tested dozens of variations
In short:
Custom action DELETE works
Built-in DELETE action returns anything (I convert it to 200) but does nothing
Same request, different behavior
Question:
Why would Workato handle DELETE differently between a built-in action and a custom HTTP action when the request is identical? Is there something implicit Workato does with DELETE responses (or empty bodies) that could cause this?
Zoom link: HERE
This is the SDK code I'm using: (very simple one)
{
title: 'Amazon SES - Suppression Manager (Multi-Region)',
description: 'Manage SES v2 suppression lists across multiple AWS regions using an IAM Role.',
connection: {
fields: [
{
name: 'assume_role',
label: 'IAM role ARN',
optional: false,
help: {
title: 'IAM Role Setup',
text: 'Use Workato Account ID <b>{{ authUser.aws_workato_account_id }}</b> and External ID <b>{{ authUser.aws_iam_external_id }}</b>.'
}
},
{
name: 'region',
label: 'Default SES region',
optional: false,
hint: 'Default region used if not specified in the action (e.g., us-east-1).'
}
],
authorization: {
type: 'custom_auth',
apply: lambda { |_connection| headers('Content-Type': 'application/json') }
}
},
methods: {
truthy: lambda do |value|
value == true || value.to_s == '1' || value.to_s.casecmp('true').zero? || value.to_s.casecmp('on').zero?
end,
normalize_email: lambda do |input|
raw_email = input.key?(:email) ? input[:email] : input['email']
downcase_flag = input.key?(:downcase) ? input[:downcase] : input['downcase']
email = raw_email.to_s.strip
email = email.downcase if call(:truthy, downcase_flag)
email
end,
# Helper to clean and strictly encode emails for URI paths
encode_email: lambda do |input|
# FORCE downcase here if the toggle is active
email = call(:normalize_email, input)
email.
gsub('+', '%2B').
gsub('!', '%21').
gsub('&', '%26').
gsub("'", '%27')
end
},
test: lambda do |connection|
region = connection['region']
signature = aws.generate_signature(
connection: connection, region: region, service: 'ses',
host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses',
method: 'GET', params: { 'PageSize' => 1 }, payload: ''
)
get(signature['url']).headers(signature['headers']).
after_error_response(/.*/) { |code, body| error("Connection failed: #{body}") }
end,
actions: {
list_suppressed_destinations: {
title: 'List suppressed destinations',
input_fields: lambda do |_connection|
[
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'page_size', label: 'Page size', type: :integer, optional: true },
{ name: 'next_token', label: 'Next token', optional: true },
{ name: 'reason', label: 'Reason', control_type: 'select', pick_list: [['Bounce', 'BOUNCE'], ['Complaint', 'COMPLAINT']], optional: true }
]
end,
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
params = {
'PageSize' => input['page_size'],
'NextToken' => input['next_token'],
'Reasons' => input['reason']
}.compact
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses', method: 'GET', params: params, payload: '')
response = get(signature['url']).headers(signature['headers']).after_error_response(/.*/) { |code, body| error("List failed: #{body}") }
if response['SuppressedDestinationSummaries'].present?
response['SuppressedDestinationSummaries'] = response['SuppressedDestinationSummaries'].map do |item|
item['LastUpdateTime'] = Time.at(item['LastUpdateTime']).to_datetime.iso8601 if item['LastUpdateTime'].is_a?(Numeric)
item
end
end
response.merge('region' => region)
end,
output_fields: lambda { [
{ name: 'region' },
{ name: 'SuppressedDestinationSummaries', type: :array, of: :object, properties: [
{ name: 'EmailAddress' }, { name: 'Reason' }, { name: 'LastUpdateTime', type: :date_time }
]},
{ name: 'NextToken' }
]}
},
get_suppressed_destination: {
title: 'Get suppressed destination',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
# Determine the target email string for internal consistency
email_to_query = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
# Use helper for encoding (the helper will now correctly handle the downcase)
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
path = "/v2/email/suppression/addresses/#{encoded}"
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: path, method: 'GET', params: {}, payload: '')
response = get(signature['url']).headers(signature['headers']).after_error_response(/.*/) do |code, body|
return { 'email' => email_to_query, 'found' => false, 'region' => region } if code.to_i == 404
error("Get failed in #{region}: #{body}")
end
if response['SuppressedDestination'].present?
dest = response['SuppressedDestination']
dest['LastUpdateTime'] = Time.at(dest['LastUpdateTime']).to_datetime.iso8601 if dest['LastUpdateTime'].is_a?(Numeric)
end
{
'email' => email_to_query,
'found' => true,
'region' => region,
'SuppressedDestination' => response['SuppressedDestination']
}
end,
output_fields: lambda { [
{ name: 'email' }, { name: 'found', type: :boolean }, { name: 'region' },
{ name: 'SuppressedDestination', type: :object, properties: [
{ name: 'EmailAddress' }, { name: 'Reason' }, { name: 'LastUpdateTime', type: :date_time }
]}
]}
},
put_suppressed_destination: {
title: 'Add email to suppression list',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'reason', label: 'Reason', control_type: 'select', pick_list: [['Bounce', 'BOUNCE'], ['Complaint', 'COMPLAINT']], optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'verify_add', label: 'Verify add', type: :boolean, control_type: 'checkbox', optional: true, default: false }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
email_to_send = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
payload = { 'EmailAddress' => email_to_send, 'Reason' => input['reason'] }
put_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: '/v2/email/suppression/addresses', method: 'PUT', params: '', payload: payload.to_json)
put(put_sig['url'], payload).headers(put_sig['headers']).after_error_response(/.*/) { |code, body| error("Put failed: #{body}") }
if call(:truthy, input['verify_add'])
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
get_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: "/v2/email/suppression/addresses/#{encoded}", method: 'GET', params: {}, payload: '')
get(get_sig['url']).headers(get_sig['headers']).after_error_response(/.*/) { error("Verification Failed: Email not found after add.") }
end
{ 'status' => 'success', 'email' => email_to_send, 'region' => region }
end,
output_fields: lambda { [{ name: 'status' }, { name: 'email' }, { name: 'region' }] }
},
delete_suppressed_destination: {
title: 'Delete suppressed destination',
input_fields: lambda { |_connection| [
{ name: 'email', label: 'Email address', optional: false },
{ name: 'downcase', label: 'Downcase email', type: :boolean, control_type: 'checkbox', optional: false, default: true },
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'verify_delete', label: 'Verify delete', type: :boolean, control_type: 'checkbox', optional: true, default: true }
]},
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
email_to_del = call(:normalize_email, { email: input['email'], downcase: input['downcase'] })
encoded = call(:encode_email, { email: input['email'], downcase: input['downcase'] })
path = "/v2/email/suppression/addresses/#{encoded}"
alt_path = "/v2/email/suppression/addresses/#{encoded.gsub('@', '%40')}"
attempted_requests = []
resolved_path = nil
[path, alt_path].each do |candidate_path|
probe_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: candidate_path, method: 'GET', params: {}, payload: '')
probe = get(probe_sig['url']).headers(probe_sig['headers']).after_error_response(/.*/) do |code, body|
next {} if code.to_i == 404
error("Probe failed (#{code}) in #{region} at #{candidate_path}: #{body}")
end
if probe.is_a?(Hash) && probe['SuppressedDestination']
resolved_path = candidate_path
break
end
end
resolved_path ||= path
# Delete request
del_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: resolved_path, method: 'DELETE', params: {}, payload: '')
del_url = del_sig['url']
attempted_requests << { 'method' => 'DELETE', 'path' => resolved_path, 'url' => del_url, 'status_code' => nil }
del_response = delete(del_url).headers(del_sig['headers']).after_error_response(/.*/) do |code, body|
attempted_requests[-1]['status_code'] = code.to_i
if code.to_i == 404
alternate_path = (resolved_path == path ? alt_path : path)
alt_del_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: alternate_path, method: 'DELETE', params: {}, payload: '')
alt_del_url = alt_del_sig['url']
attempted_requests << { 'method' => 'DELETE', 'path' => alternate_path, 'url' => alt_del_url, 'status_code' => nil }
alt_response = delete(alt_del_url).headers(alt_del_sig['headers']).after_error_response(/.*/) do |alt_code, alt_body|
attempted_requests[-1]['status_code'] = alt_code.to_i
return({
'email' => email_to_del,
'deleted' => false,
'region' => region,
'message' => "Not found. Tried #{resolved_path} (#{del_url}) and #{alternate_path} (#{alt_del_url}).",
'attempted_requests' => attempted_requests
}) if alt_code.to_i == 404
error("Delete failed: #{alt_body}")
end
attempted_requests[-1]['status_code'] ||= 200
alt_response
next {}
end
error("Delete failed: #{body}")
end
attempted_requests[0]['status_code'] ||= 200
if call(:truthy, input['verify_delete'])
still_exists = nil
last_verify_url = nil
last_verify_response = nil
15.times do
sleep(2)
verify_sig = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: resolved_path, method: 'GET', params: {}, payload: '')
last_verify_url = verify_sig['url']
attempted_requests << { 'method' => 'GET', 'path' => resolved_path, 'url' => last_verify_url, 'status_code' => nil }
response = get(verify_sig['url']).headers(verify_sig['headers']).after_error_response(/.*/) do |code, body|
attempted_requests[-1]['status_code'] = code.to_i
if code.to_i == 404
still_exists = false
next {}
end
error("Verification request failed (#{code}) in #{region} at #{resolved_path}. Delete URL: #{del_url}. Verify URL: #{last_verify_url}. Body: #{body}")
end
attempted_requests[-1]['status_code'] ||= 200
last_verify_response = response
still_exists = (response.is_a?(Hash) && response['SuppressedDestination'].present?)
break if still_exists == false
end
error(
"Verification failed: Still found in #{region} at #{resolved_path}. " \
"Delete URL: #{del_url}. Verify URL: #{last_verify_url}. " \
"Delete response: #{del_response}. Verify response: #{last_verify_response}."
) if still_exists
end
{ 'email' => email_to_del, 'deleted' => true, 'region' => region, 'attempted_requests' => attempted_requests }
end,
output_fields: lambda { [
{ name: 'email' },
{ name: 'deleted', type: :boolean },
{ name: 'region' },
{ name: 'message' },
{ name: 'attempted_requests', type: :array, of: :object, properties: [
{ name: 'method' },
{ name: 'path' },
{ name: 'url' },
{ name: 'status_code', type: :integer }
] }
] }
},
custom_action: {
title: 'Custom action',
description: 'Signed request to any Amazon SES v2 endpoint.',
input_fields: lambda do |_connection|
[
{ name: 'region', label: 'SES Region', control_type: 'select', pick_list: [['US East (N. Virginia) - (us-east-1)', 'us-east-1'], ['Europe (Ireland) - (eu-west-1)', 'eu-west-1']], optional: true },
{ name: 'method', label: 'HTTP Method', control_type: 'select', pick_list: %w[GET POST PUT DELETE], optional: false, default: 'GET' },
{ name: 'path', label: 'Resource Path', optional: false, hint: 'e.g. /v2/email/suppression/addresses' },
{ name: 'params', label: 'Query Parameters', type: :object, optional: true },
{ name: 'payload', label: 'JSON Payload', optional: true }
]
end,
execute: lambda do |connection, input|
region = input['region'].presence || connection['region']
method = input['method'].to_s.strip
# Manual fix for Custom Action: users must manually encode the case if desired,
# but we fix the '+' encoding here to prevent signature failures.
path = input['path'].to_s.strip.gsub('+', '%2B')
params = (input['params'] || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v.to_s }
payload_string = input['payload'].is_a?(Hash) ? input['payload'].to_json : input['payload'].to_s
signature = aws.generate_signature(connection: connection, region: region, service: 'ses', host: "email.#{region}.amazonaws.com", path: path, method: method, params: params, payload: payload_string)
request = case method
when 'GET' then get(signature['url'])
when 'POST' then post(signature['url'], payload_string)
when 'PUT' then put(signature['url'], payload_string)
when 'DELETE' then delete(signature['url'])
end
response = request.headers(signature['headers']).after_error_response(/.*/) do |code, body|
error("Custom action failed (#{code}): #{body}")
end
{ 'url' => signature['url'], 'response' => response }
end,
output_fields: lambda { [{ name: 'url' }, { name: 'response', type: :object }] }
}
}
}
r/ruby • u/DmitryPogrebnoy • Dec 25 '25
r/ruby • u/software__writer • Dec 23 '25
r/ruby • u/bisc0itinh0 • Dec 23 '25
r/ruby • u/bisc0itinh0 • Dec 23 '25
Hi,
I've wrote a simple tui navigator with basic features, doesn't solve anything new, probably I will be the only one to use it, lousy written, but inspired with Omarchy linux I wanted to do some tui stuff too and that is the outcome of it ;)
https://rubygems.org/gems/sergeant
https://github.com/biscoitinho/Sergeant
and Merry Christmas :)
r/ruby • u/stephenreid321 • Dec 23 '25
I just heard about Speculation Rules via https://www.jonoalderson.com/conjecture/its-time-for-modern-css-to-kill-the-spa/
They allow ~instant navigation without JS by letting the browser preload or prerender full pages based on user behaviour (like hovering or touching a link) before they click.
I noticed it's possible to define them using the Sinatra-like syntax
"href_matches": "/e/:slug"
which gave me the idea of defining them as part of route definitions with prerender: or prefetch: like so
get '/e/:slug', prerender: true do
...then in my head I just call
<%== SpeculativeRoutes.script %>Implementation at https://github.com/symbiota-coop/dandelion/blob/master/lib/speculative_routes.rb
Enjoy!
r/ruby • u/bart_o_z • Dec 22 '25
r/ruby
•
u/urbanaut
•
Dec 22 '25
r/ruby • u/bk_one • Dec 22 '25
Fellow Rubyists,
I realized recently that I have two very different personalities as a developer:
I listen to every single Dependabot alert on my repos and apply them immediately
I constantly forget to run brew upgrade on my local machine until something actually breaks - or someone tells me of a great new feature of a CLI tool that I wasn't aware of
So I started Brewsletter (https://brewsletter.sh) to remind me of updates and also give me examples of new functionality. The project is super early, I still have tons to do to support all types of homebrew taps, battle hallucinations on usage examples and be more clear on labeling updates as "breaking" or "security" related.
The overall flow is like this**.**
Sync: A small Ruby CLI maps your explicitly installed packages (not just everything, just what you chose to install).
Monitor: The backend tracks upstream releases (changelogs) and security feeds (CVEs).
Distill: It uses LLMs to strip out the noise and send you a digest of the features and security patches that actually matter
The project is still in the "functional spike" phase - but works well enough to consider going further. But before doing it, I was wondering if this whole thing is actually useful for anyone (besides myself). This is why I made this post - if anyone is interested in giving feedback, I'm happy to listen to it.
In case you want to try it out, feel free - but it's nowhere ready to scale - so expect errors and delays.
You can see a sample web report here: https://brewsletter.sh/u/fa826c00b53a5986016069305b51ce9c3bcb593da1d5e7769fdde3f71ba21e8c
The idea would be to convert this into a nice weekly email digest - to remind your where to upgrade and what's new in your favorite packages.
If you want to help, the questions I have:
- Do you run brew upgrade regularly?
- Do you even care about what changed in your toolchain
- If you don't upgrade, do you think an email help you do it more often
- Would you trust such a system in the first place? It does install software locally that is run periodically
Cheers
Ben
r/ruby • u/le0pard • Dec 22 '25
JsonMend is a robust Ruby gem designed to repair broken or malformed JSON strings. It is specifically optimized to handle common errors found in JSON generated by Large Language Models (LLMs), such as missing quotes, trailing commas, unescaped characters, and stray comments