Morphix
A concise, expressive DSL for elegantly reshaping and transforming Ruby hashes and JSON structures.
Installation
Add this line to your application's Gemfile:
gem "morphix"
And then execute:
$ bundle install
Or install it yourself as:
$ gem install morphix
Core Features
-
Fluent DSL: Clean, readable chainable methods (
rename
,map
,reject
,reshape
,map_collection
) - Nested Transformation: Easily define transformations inside nested hashes or collections
- Reusable Transformers: Define transformations once, reuse them multiple times
- Functional Style: Immutable by default—returns new objects, leaving input untouched
- Flexible & Extensible: Easy to add custom transformations via Ruby blocks
- Error Handling: Robust error handling for edge cases and invalid data
- Performance Optimized: Efficiently handles large and complex data structures
Common Use Cases
API Response Normalization
transformer = Morphix::Transformer.new do
rename :user_full_name, to: :name
map :created_at do |timestamp|
Time.parse(timestamp)
end
reshape :address do
rename :postal_code, to: :postcode
map :coordinates do |coords|
{ lat: coords[:lat].to_f, lng: coords[:lng].to_f }
end
end
end
# Use it to normalize API responses
response = api_client.get_user(123)
normalized_data = transformer.apply(response)
Data Migration
transformer = Morphix::Transformer.new do
map_collection :records do
rename :legacy_id, to: :id
map :status do |status|
case status
when "ACTIVE" then "active"
when "INACTIVE" then "inactive"
else "unknown"
end
end
reshape :metadata do
map :created_at, &:to_s
map :updated_at, &:to_s
end
end
end
# Transform legacy data format to new format
legacy_data = load_legacy_records()
new_data = transformer.apply(legacy_data)
Data Sanitization
transformer = Morphix::Transformer.new do
reject :password, :ssn, :credit_card
map :email do |email|
email.to_s.downcase.strip
end
map :phone do |phone|
phone.to_s.gsub(/[^0-9]/, "")
end
end
# Sanitize user input before processing
user_data = get_user_input()
sanitized_data = transformer.apply(user_data)
Usage
Morphix provides a simple DSL for transforming data structures. Here are some examples:
Basic Transformations
Renaming Keys
transformer = Morphix::Transformer.new do
rename :old_name, to: :new_name
end
input = { old_name: "Dave" }
result = transformer.apply(input)
# => { new_name: "Dave" }
Transforming Values
transformer = Morphix::Transformer.new do
map :age, &:to_i
end
input = { age: "40" }
result = transformer.apply(input)
# => { age: 40 }
Removing Keys
transformer = Morphix::Transformer.new do
reject :password
end
input = { name: "Dave", password: "secret" }
result = transformer.apply(input)
# => { name: "Dave" }
Nested Transformations
Transforming Nested Hashes
transformer = Morphix::Transformer.new do
reshape :address do
rename :postal_code, to: :postcode
map :verified do |value|
value == "true"
end
end
end
input = {
address: {
street: "Main St",
postal_code: "S2",
verified: "true"
}
}
result = transformer.apply(input)
# => {
# address: {
# street: "Main St",
# postcode: "S2",
# verified: true
# }
# }
Transforming Collections
transformer = Morphix::Transformer.new do
map_collection :users do
rename :username, to: :name
reject :internal_notes
end
end
input = {
users: [
{ username: "Dave", internal_notes: "VIP" },
{ username: "Jason", internal_notes: "Banned" }
]
}
result = transformer.apply(input)
# => {
# users: [
# { name: "Dave" },
# { name: "Jason" }
# ]
# }
Complex Transformations
Combining Multiple Transformations
transformer = Morphix::Transformer.new do
rename :full_name, to: :name
map :age, &:to_i
reject :password
reshape :address do
rename :postal_code, to: :postcode
map :coordinates do |coords|
{ lat: coords[:lat].to_f, lng: coords[:lng].to_f }
end
end
end
input = {
full_name: "Dave Cooper",
age: "40",
password: "secret",
address: {
street: "123 Main St",
postal_code: "12345",
coordinates: { lat: "40.7128", lng: "-74.0060" }
}
}
result = transformer.apply(input)
# => {
# name: "Dave Cooper",
# age: 40,
# address: {
# street: "123 Main St",
# postcode: "12345",
# coordinates: { lat: 40.7128, lng: -74.0060 }
# }
# }
Advanced Features
Conditional Transformations
transformer = Morphix::Transformer.new do
map :status do |status|
case status
when "active" then 1
when "pending" then 0
when "deleted" then -1
else nil
end
end
end
input = { status: "active" }
result = transformer.apply(input)
# => { status: 1 }
Complex Data Type Conversions
transformer = Morphix::Transformer.new do
map :dates do |dates|
dates.transform_values { |v| Time.strptime(v, "%Y-%m-%d %H:%M:%S") }
end
map :numbers do |nums|
nums.transform_values(&:to_f)
end
map :flags do |flags|
flags.transform_values { |v| v.to_s.downcase == "true" }
end
end
input = {
dates: { created: "2024-03-27 10:30:00", updated: "2024-03-28 15:45:00" },
numbers: { latitude: "40.7128", longitude: "-74.0060" },
flags: { active: "True", deleted: "FALSE", archived: "true" }
}
result = transformer.apply(input)
# => {
# dates: {
# created: #<Time 2024-03-27 10:30:00>,
# updated: #<Time 2024-03-28 15:45:00>
# },
# numbers: { latitude: 40.7128, longitude: -74.0060 },
# flags: { active: true, deleted: false, archived: true }
# }
Handling Complex Data Structures
transformer = Morphix::Transformer.new do
reshape :organization do
map_collection :departments do
rename :dept_name, to: :name
map_collection :employees do
rename :full_name, to: :name
map :age, &:to_i
reject :ssn
end
end
end
end
input = {
organization: {
departments: [
{
dept_name: "Engineering",
employees: [
{ full_name: "Dave Cooper", age: "35", ssn: "123-45-6789" },
{ full_name: "Alice Smith", age: "28", ssn: "987-65-4321" }
]
}
]
}
}
result = transformer.apply(input)
# => {
# organization: {
# departments: [
# {
# name: "Engineering",
# employees: [
# { name: "Dave Cooper", age: 35 },
# { name: "Alice Smith", age: 28 }
# ]
# }
# ]
# }
# }
Error Handling
Morphix provides robust error handling for various edge cases:
transformer = Morphix::Transformer.new do
map :number do |n|
Integer(n)
end
end
input = { number: "not_a_number" }
begin
transformer.apply(input)
rescue ArgumentError => e
puts "Invalid number format: #{e.message}"
end
Performance Considerations
Morphix is designed to handle large and complex data structures efficiently:
transformer = Morphix::Transformer.new do
map_collection :numbers do
map :value, &:to_i
end
end
# Handles large arrays efficiently
large_array = 1000.times.map { |i| { value: i.to_s } }
input = { numbers: large_array }
result = transformer.apply(input)
Troubleshooting
Common Issues
-
Missing Keys
# If a key doesn't exist, it's simply ignored transformer = Morphix::Transformer.new do rename :missing_key, to: :new_key end input = { other_key: "value" } result = transformer.apply(input) # => { other_key: "value" }
-
Nil Values
# Handle nil values safely transformer = Morphix::Transformer.new do map :age do |value| value&.to_i end end input = { age: nil } result = transformer.apply(input) # => { age: nil }
-
Invalid Data Types
# Handle unexpected data types gracefully transformer = Morphix::Transformer.new do reshape :config do rename :api_key, to: :key end end input = { config: "invalid" } result = transformer.apply(input) # => { config: "invalid" }
Best Practices
-
Keep Transformers Focused
# Good: Single responsibility user_transformer = Morphix::Transformer.new do rename :username, to: :name map :age, &:to_i end # Bad: Too many responsibilities transformer = Morphix::Transformer.new do rename :username, to: :name map :age, &:to_i reshape :address do rename :postal_code, to: :postcode end map_collection :orders do map :total, &:to_f end end
-
Use Composition for Complex Transformations
# Break down complex transformations into smaller, reusable parts name_transformer = Morphix::Transformer.new do rename :full_name, to: :name do |name| first, last = name.split { first: first, last: last } end end address_transformer = Morphix::Transformer.new do reshape :address do rename :postal_code, to: :postcode map :coordinates do |coords| { lat: coords[:lat].to_f, lng: coords[:lng].to_f } end end end # Combine transformers for complex transformations combined_transformer = Morphix::Transformer.new do rename :full_name, to: :name do |name| first, last = name.split { first: first, last: last } end reshape :address do rename :postal_code, to: :postcode map :coordinates do |coords| { lat: coords[:lat].to_f, lng: coords[:lng].to_f } end end end
-
Handle Edge Cases Explicitly
transformer = Morphix::Transformer.new do map :status do |status| case status&.to_s&.downcase when "active", "1", "true" then "active" when "inactive", "0", "false" then "inactive" else "unknown" end end end
Development
After checking out the repo, run bundle install
to install dependencies. Then, run rake spec
to run the tests.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/OkayDave/morphix.
License
The gem is available as open source under the terms of the MIT License.