philiprehberger-safe_yaml

Tests Gem Version Last updated

Safe YAML loading with restricted types, schema validation, and size limits

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-safe_yaml"

Or install directly:

gem install philiprehberger-safe_yaml

Usage

require "philiprehberger/safe_yaml"

# Load a YAML string safely
data = Philiprehberger::SafeYaml.load("name: Alice\nage: 30\n")
# => {"name"=>"Alice", "age"=>30}

# Load from a file
data = Philiprehberger::SafeYaml.load_file("config.yml")

Size Limits

# Reject oversized input
Philiprehberger::SafeYaml.load(huge_string, max_size: 1024)
# => raises Philiprehberger::SafeYaml::SizeError if input exceeds 1024 bytes

Permitted Classes

# Allow specific classes during deserialization
data = Philiprehberger::SafeYaml.load(yaml_string, permitted_classes: [Symbol, Date])

Safe Serialization

require "philiprehberger/safe_yaml"

data = { "host" => "localhost", "port" => 3000, "debug" => true }
yaml_string = Philiprehberger::SafeYaml.dump(data)
Philiprehberger::SafeYaml.dump_file(data, "config.yml")

Schema Validation

schema = Philiprehberger::SafeYaml::Schema.new do
  required :name, String
  required :age, Integer
  optional :email, String
end

data = Philiprehberger::SafeYaml.load("name: Alice\nage: 30\n")

# Validate with exceptions
schema.validate!(data)
# => true (or raises SchemaError)

# Validate without exceptions
result = schema.validate(data)
# => { valid: true, errors: [] }

Alias Limits

# Block all aliases (default behavior)
Philiprehberger::SafeYaml.load(yaml_with_aliases)
# => raises Psych::BadAlias

# Allow up to 3 aliases
data = Philiprehberger::SafeYaml.load(yaml_with_aliases, max_aliases: 3)

# Exceeding the limit raises an error
Philiprehberger::SafeYaml.load(yaml_with_many_aliases, max_aliases: 2)
# => raises Philiprehberger::SafeYaml::Error

Load and Validate

schema = Philiprehberger::SafeYaml::Schema.new do
  required :name, String
  required :port, Integer
end

# Parse and validate in one step
data = Philiprehberger::SafeYaml.load_and_validate("name: app\nport: 3000\n", schema: schema)
# => {"name"=>"app", "port"=>3000}

# Raises SchemaError if validation fails
Philiprehberger::SafeYaml.load_and_validate("name: app\n", schema: schema)
# => raises Philiprehberger::SafeYaml::SchemaError

Sanitize

raw = "# This is a comment\nname: Alice\n# Another comment\nage: 30\n"
cleaned = Philiprehberger::SafeYaml.sanitize(raw)
# => "name: Alice\nage: 30\n"

Defaults Merge

defaults = { 'host' => 'localhost', 'port' => 3000, 'db' => { 'pool' => 5, 'timeout' => 30 } }
yaml = "port: 8080\ndb:\n  pool: 10\n"

data = Philiprehberger::SafeYaml.load_with_defaults(yaml, defaults: defaults)
# => {"host"=>"localhost", "port"=>8080, "db"=>{"pool"=>10, "timeout"=>30}}

Custom Validation Rules

schema = Philiprehberger::SafeYaml::Schema.new do
  required :port, Integer, rule: ->(v) { (1..65_535).cover?(v) }, message: 'must be between 1 and 65535'
  required :status, String, rule: ->(v) { %w[active inactive].include?(v) }
  optional :email, String, rule: ->(v) { v.include?('@') }, message: 'must be a valid email'
end

schema.validate!({ 'port' => 80, 'status' => 'active' })
# => true

result = schema.validate({ 'port' => 0, 'status' => 'unknown' })
# => { valid: false, errors: ["key port: must be between 1 and 65535", "key status: failed validation rule"] }

API

Method / Class Description
SafeYaml.load(string, **opts) Safely load a YAML string
SafeYaml.load_file(path, **opts) Safely load a YAML file
SafeYaml.load_and_validate(string, schema:, **opts) Load and validate in one step
SafeYaml.load_with_defaults(string, defaults:, **opts) Load and deep merge over defaults
SafeYaml.sanitize(string) Strip comments and normalize whitespace
SafeYaml.dump(data, permitted_classes:) Safely dump data to a YAML string
SafeYaml.dump_file(data, path, permitted_classes:) Safely dump data to a YAML file
Loader.load(string, permitted_classes:, max_aliases:, max_size:) Core safe loading with all options
Loader.load_file(path, **opts) Read file and delegate to Loader.load
Loader.dump(data, permitted_classes:) Dump data to YAML with type validation
Loader.dump_file(data, path, permitted_classes:) Write validated YAML to file
Schema.new(&block) Define a validation schema with DSL
Schema#required(key, type, rule:, message:) Declare a required key with expected type and optional validation rule
Schema#optional(key, type, rule:, message:) Declare an optional key with expected type and optional validation rule
Schema#validate!(data) Validate and raise SchemaError on failure
Schema#validate(data) Validate and return { valid:, errors: }
SafeYaml::Error Base error class
SafeYaml::SchemaError Raised on schema validation failure
SafeYaml::SizeError Raised when input exceeds max_size

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