Project

morphix

0.0
The project is in a healthy, maintained state
Morphix provides a clear, expressive DSL for transforming data structures in Ruby. Perfect for API response normalization, JSON reshaping, and ETL pipelines.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

~> 0.2.0
 Project Readme

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

  1. 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" }
  2. 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 }
  3. 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

  1. 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
  2. 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
  3. 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.