philiprehberger-toml_kit
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_kitUsage
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"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 outputSchema 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 invalidMerging
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 conflictQuery 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") # => 30Type 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"] # => :helloTOML 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.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 rubocopSupport
If you find this project useful: