philiprehberger-toml_kit

Tests Gem Version Last updated

TOML v1.0 parser and serializer for Ruby with comment preservation, schema validation, merging, querying, type coercion, and diffing

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-toml_kit"

Or install directly:

gem install philiprehberger-toml_kit

Usage

require "philiprehberger/toml_kit"

data = Philiprehberger::TomlKit.parse('title = "TOML Example"')
# => {"title" => "TOML Example"}

Parsing Strings

toml = <<~TOML
  [database]
  host = "localhost"
  port = 5432
  enabled = true

  [[servers]]
  name = "alpha"
  port = 8001

  [[servers]]
  name = "beta"
  port = 8002
TOML

config = Philiprehberger::TomlKit.parse(toml)
config["database"]["host"]   # => "localhost"
config["servers"][0]["name"]  # => "alpha"

Validity Check

Check whether a string is valid TOML without raising:

Philiprehberger::TomlKit.valid?('key = "value"')  # => true
Philiprehberger::TomlKit.valid?('key = [broken')  # => false

Loading Files

config = Philiprehberger::TomlKit.load("config.toml")

Serializing to TOML

hash = {
  "title" => "My App",
  "database" => { "host" => "localhost", "port" => 5432 },
  "servers" => [
    { "name" => "alpha", "port" => 8001 },
    { "name" => "beta", "port" => 8002 }
  ]
}

toml_string = Philiprehberger::TomlKit.dump(hash)

Saving to Files

Philiprehberger::TomlKit.save(hash, "output.toml")

Supported Types

All TOML v1.0 types are supported:

toml = <<~TOML
  str = "hello"
  int = 42
  hex = 0xDEADBEEF
  oct = 0o755
  bin = 0b11010110
  flt = 3.14
  inf_val = inf
  bool = true
  dt = 1979-05-27T07:32:00Z
  date = 1979-05-27
  time = 07:32:00
  arr = [1, 2, 3]
  inline = {x = 1, y = 2}
TOML

data = Philiprehberger::TomlKit.parse(toml)

Comment Preservation

Parse a TOML document while preserving comments for round-trip editing:

toml = <<~TOML
  # Application config
  title = "My App"

  # Database settings
  [database]
  host = "localhost" # primary host
TOML

doc = Philiprehberger::TomlKit.parse_with_comments(toml)
doc["title"]                    # => "My App"
doc["database"]["host"]         # => "localhost" (via doc.data)
doc.header_comments             # => ["# Application config"]
doc.comments["database.host"]   # => {inline: "# primary host"}

# Modify and re-serialize with comments intact
doc["title"] = "New App"
output = doc.to_toml
# Comments are preserved in the output

Schema Validation

Define expected structure and validate parsed TOML against it:

schema = Philiprehberger::TomlKit::Schema.new(
  "name" => { type: String, required: true },
  "port" => { type: Integer, required: true },
  "database" => {
    type: Hash,
    required: true,
    properties: {
      "host" => { type: String, required: true },
      "port" => { type: Integer }
    }
  },
  "tags" => { type: Array, items: { type: String } }
)

data = Philiprehberger::TomlKit.parse(toml_string)
errors = schema.validate(data)
# => [] if valid, or ["Missing required key: name", ...]

schema.validate!(data) # raises SchemaError if invalid

Merging

Deep merge two TOML hashes with conflict resolution:

base = Philiprehberger::TomlKit.parse(base_toml)
overrides = Philiprehberger::TomlKit.parse(override_toml)

# Right-side wins (default)
merged = Philiprehberger::TomlKit.merge(base, overrides)

# Left-side wins
merged = Philiprehberger::TomlKit.merge(base, overrides, strategy: :keep_existing)

# Raise on conflict
merged = Philiprehberger::TomlKit.merge(base, overrides, strategy: :error_on_conflict)
# raises MergeConflictError if keys conflict

Query Support

Access nested values using dot-paths:

data = Philiprehberger::TomlKit.parse(toml_string)

Philiprehberger::TomlKit.query(data, "database.host")
# => "localhost"

Philiprehberger::TomlKit.query(data, "servers[0].name")
# => "alpha"

Philiprehberger::TomlKit.query(data, "missing.path", default: "N/A")
# => "N/A"

# Additional Query methods
Philiprehberger::TomlKit::Query.set(data, "database.timeout", 30)
Philiprehberger::TomlKit::Query.exists?(data, "database.host")  # => true
Philiprehberger::TomlKit::Query.delete(data, "database.timeout") # => 30

Type Coercion Hooks

Register custom serializers and deserializers for Ruby types:

coercion = Philiprehberger::TomlKit::TypeCoercion.new

coercion.register(
  Symbol,
  tag: "symbol",
  serializer: ->(v) { v.to_s },
  deserializer: ->(v) { v.to_sym }
)

# Serialize: converts symbols to tagged strings
data = { "key" => :hello }
serialized = coercion.coerce_for_serialize(data)
toml = Philiprehberger::TomlKit.dump(serialized)

# Deserialize: converts tagged strings back to symbols
parsed = Philiprehberger::TomlKit.parse(toml)
restored = coercion.coerce_for_deserialize(parsed)
restored["key"] # => :hello

TOML Diff

Compare two TOML documents and report differences:

old_config = Philiprehberger::TomlKit.parse(old_toml)
new_config = Philiprehberger::TomlKit.parse(new_toml)

changes = Philiprehberger::TomlKit.diff(old_config, new_config)
changes.each do |change|
  puts "#{change.type}: #{change.path}"
  # => :added, :removed, or :changed
end

# Filter by type
Philiprehberger::TomlKit::Diff.additions(old_config, new_config)
Philiprehberger::TomlKit::Diff.removals(old_config, new_config)
Philiprehberger::TomlKit::Diff.changes(old_config, new_config)

# Check equality
Philiprehberger::TomlKit::Diff.identical?(old_config, new_config)

API

Method Description
TomlKit.parse(string) Parse a TOML string into a Hash
TomlKit.valid?(string) Return true if the string parses as valid TOML
TomlKit.load(path) Parse a TOML file into a Hash
TomlKit.dump(hash) Serialize a Hash to a TOML string
TomlKit.save(hash, path) Write a Hash as a TOML file
TomlKit.parse_with_comments(string) Parse TOML preserving comments, returns CommentDocument
TomlKit.query(data, path, default:) Dot-path access into nested hashes
TomlKit.merge(left, right, strategy:) Deep merge two hashes with conflict resolution
TomlKit.diff(left, right) Compare two hashes, returns array of Diff::Change
Schema.new(properties) Create a schema for validation
Schema#validate(data) Validate data, returns array of error strings
Schema#validate!(data) Validate data, raises SchemaError on failure
Query.get(data, path, default:) Retrieve nested value by dot-path
Query.set(data, path, value) Set nested value by dot-path
Query.exists?(data, path) Check if a dot-path exists
Query.delete(data, path) Delete value at dot-path
Diff.diff(left, right) Full diff between two hashes
Diff.additions(left, right) Keys added in right
Diff.removals(left, right) Keys removed from left
Diff.changes(left, right) Keys with changed values
Diff.identical?(left, right) Check if two hashes are equal
TypeCoercion#register(type, ...) Register custom type handler
TypeCoercion#coerce_for_serialize(value) Apply serialization coercions
TypeCoercion#coerce_for_deserialize(value) Apply deserialization coercions
Merger.merge(left, right, strategy:) Merge with strategy

Development

bundle install
bundle exec rspec
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT