Skip to content

Instantly share code, notes, and snippets.

@keithschacht
Last active October 29, 2024 20:36
Show Gist options
  • Select an option

  • Save keithschacht/67d4c0bd523bff067ca20140b8172cbf to your computer and use it in GitHub Desktop.

Select an option

Save keithschacht/67d4c0bd523bff067ca20140b8172cbf to your computer and use it in GitHub Desktop.
This code creates a new declaration, "df", to be used instead of "def" for those who want their Ruby methods to be typed. "df" supports passing in type declarations as part of the method declaration. I called this experiment Gelato since it's an improvement on Ruby's Sorbet. To run this example: (1) save all three files into a directory, (2) ens…
# typed: true
require 'sorbet-runtime'
require_relative 'gelato'
extend T::Sig
# Include "gelato" for a better syntax than Sorbet. Gelato is merely a DSL which writes Sorbet type definitions.
#
# Instead of the Sorbet two-line syntax:
#
# sig { params(name: T.untyped, nickname: String, member: T::Boolean, age: Integer, gender: T.untyped).returns(Integer) }
# def greet(name, nickname, member, age:, gender:)
#
# You can write this syntax:
df :greet, :name, nickname: [String], member: [T::Boolean], age: Integer, gender: T.untyped, returns: Integer do
puts "Hello, #{name}! Your nickname: #{nickname}, Member? #{member}, Age: #{age}, Gender: #{gender}"
name.length
end
result = greet("Samantha", "Sam", true, age: 30, gender: "female")
# Try passing invalid types to this greet() method and you'll see Sorbet runtime checks executing.
# To get static type checking working, save the "srb" file (3rd file in gist), make it executable, and run it ./srb
puts "Name length: #{result}"
# NOTES:
#
# 1) The first three arguments are positional and the last three arguments are named. :name was obviously positional,
# but nickname indicates it's positional with the brackets around [String]. Contrasted this with age: Integer
#
# The first positional argument, :name, is untyped but only named arguments require explicitly declaring that. This
# is so simple methods can stay simple. e.g. An typed method with a single argument is:
#
# df :expired?, :age do
# 2) Gelato needs to be extended in order to support default values for arguments and to support compound values
# This was my syntax idea for support default values:
#
# df :greet, :name, nickname: [String: "nick"], member: [T::Boolean], paid: [false], age: Integer, gender: "male", region: { String: "TX" }, country: String
#
# The first 4 arguments are positional (demonstrating w/ and w/o types and default values)
# The last 4 arguments are named (demonstrating w/ and w/o types and default values)
# typed: false
require 'sorbet-runtime'
extend T::Sig
def df(method_name, *args, **kwargs, &block)
method_definition = df_method_definition(method_name, *args, **kwargs, &block)
singleton_class.class_eval(method_definition)
end
def df_method_definition(method_name, *args, **kwargs, &block)
sig_line = df_sig_line(method_name, *args, **kwargs)
def_line = df_def_line(method_name, *args, **kwargs)
return_type = kwargs.delete(:returns) || nil
positional_params = df_positional_params(*args) + kwargs.select { |k,v| v.is_a?(Array) }.map { |k,v| [ k, v.first ] }
named_params = kwargs.select { |k,v| !v.is_a?(Array) }.to_a
all_param_names = (positional_params + named_params).map(&:first)
singleton_class.instance_variable_set("@block_for_#{method_name}", block)
method_definition = <<-RUBY
extend T::Sig
#{sig_line}
#{def_line}
context = Struct.new(#{all_param_names.map { |name| ":#{name}" }.join(', ')}).new(#{all_param_names.join(', ')})
block = self.singleton_class.instance_variable_get("@block_for_#{method_name}")
context.instance_exec(&block)
end
RUBY
end
def df_positional_params(*args)
raise "All positional arguments should be symbols but non-symbols were provided" if args.select { |a| !a.is_a?(Symbol) }.any?
positional_params = args.map do |arg|
[ arg, T.untyped ]
end
end
def df_sig_line(method_name, *args, **kwargs)
return_type = kwargs.delete(:returns) || nil
positional_params = df_positional_params(*args) + kwargs.select { |k,v| v.is_a?(Array) }.map { |k,v| [ k, v.first ] }
named_params = kwargs.select { |k,v| !v.is_a?(Array) }.to_a
sig_params = (positional_params + named_params).map do |arg|
"#{arg.first}: #{arg.last.name}"
end.join(', ')
"sig { params(#{sig_params}).#{return_type ? "returns(#{return_type})" : "void"} }"
end
def df_def_line(method_name, *args, **kwargs)
return_type = kwargs.delete(:returns) || nil
positional_params = df_positional_params(*args) + kwargs.select { |k,v| v.is_a?(Array) }.map { |k,v| [ k, v.first ] }
named_params = kwargs.select { |k,v| !v.is_a?(Array) }.to_a
method_params = (positional_params.map do |arg|
"#{arg.first}"
end + named_params.map do |arg|
"#{arg.first}:"
end).join(', ')
"def #{method_name}(#{method_params})"
end
#!/usr/bin/env ruby
require 'fileutils'
require_relative 'gelato'
# Define the directory to scan and backup
directory = '.'
backup_directory = './tmp/backup'
# Create the backup directory if it doesn't exist
FileUtils.mkdir_p(backup_directory)
# Define a method to modify the file content
def modify_file_content(file_path)
content = File.read(file_path)
# Perform your modifications here
modified_content = content.gsub(/df\s+(.*?)\s*(do\s*$|\{$)/) do |match|
eval("df_sig_line(#{$1})") + "\n" +
eval("df_def_line(#{$1})")
end
# modified_content = content
File.write(file_path, modified_content)
end
# Backup and modify files
Dir.glob("#{directory}/**/*").each do |file|
next unless File.file?(file) # Skip directories
backup_file = file.sub(directory, backup_directory)
FileUtils.mkdir_p(File.dirname(backup_file))
FileUtils.cp(file, backup_file)
modify_file_content(file)
end
# Run Sorbet
system('srb tc')
# Restore original files
Dir.glob("#{backup_directory}/**/*").each do |backup_file|
next unless File.file?(backup_file) # Skip directories
original_file = backup_file.sub(backup_directory, directory)
FileUtils.cp(backup_file, original_file)
end
# Optionally, clean up the backup directory
FileUtils.rm_rf(backup_directory)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment