[Best practices] YAML usage in Ruby on Rails

YAML

Welcome to the Yaml Cookbook for Ruby. This version of the Yaml Cookbook focuses on the Ruby implementation of Yaml by comparing Yaml documents with their Ruby counterparts.

YAML™ is a readable text format for data structures. As you’ll see below, YAML can handle many common data types and structures. And what YAML can’t handle natively can be supported through flexible type families. For example, YAML for Ruby uses type families to support storage of regular expressions, ranges and object instances.

Collections

Nested Sequences

You can include a sequence within another sequence by giving the sequence an empty dash, followed by an indented list.

1
2
3
4
- 
- foo
- bar
- baz

Ruby code:

1
[['foo', 'bar', 'baz']] 

Inline Collections

Simple Inline Array

Sequences can be contained on a single line, using the inline syntax. Separate each entry with commas and enclose in square brackets.

1
seq: [ a, b, c ] 

Ruby code:

1
{ 'seq' => [ 'a', 'b', 'c' ] } 

Simple Inline Hash

Mapping can also be contained on a single line, using the inline syntax. Each key-value pair is separated by a colon, with a comma between each entry in the mapping. Enclose with curly braces.

1
hash: { name: Steve, foo: bar } 

Ruby code:

1
{ 'hash' => { 'name' => 'Steve', 'foo' => 'bar' } } 

Multi-line Inline Collections

Both inline sequences and inline mappings can span multiple lines, provided that you indent the additional lines.

1
2
3
4
5
6
7
languages: [ Ruby, 
Perl,
Python ]
websites: { YAML: yaml.org,
Ruby: ruby-lang.org,
Python: python.org,
Perl: use.perl.org }

Ruby code:

1
2
3
4
5
6
7
8
{ 'languages' => [ 'Ruby', 'Perl', 'Python' ], 
'websites' => {
'YAML' => 'yaml.org',
'Ruby' => 'ruby-lang.org',
'Python' => 'python.org',
'Perl' => 'use.perl.org'
}
}

Basic Types

Strings

Forcing Strings

Any YAML type can be forced into a string using the explicit !str method.

1
2
date string: !str 2001-08-01 
number string: !str 192

Ruby code:

1
2
3
4
{ 
'date string' => '2001-08-01',
'number string' => '192'
}

Single-quoted Strings

You can also enclose your strings within single quotes, which allows use of slashes, colons, and other indicators freely. Inside single quotes, you can represent a single quote in your string by using two single quotes next to each other.

1
2
3
all my favorite symbols: '#:!/%.)' 
a few i hate: '&(*'
why do i hate them?: 'it''s very hard to explain'

Ruby code:

1
2
3
4
5
{ 
'all my favorite symbols' => '#:!/%.)',
'a few i hate' => '&(*',
'why do i hate them?' => 'it\'s very hard to explain'
}

Double-quoted Strings

Enclosing strings in double quotes allows you to use escapings to represent ASCII and Unicode characters.

1
i know where i want my line breaks: "one here\nand another here\n" 

Ruby code:

1
2
3
{ 
'i know where i want my line breaks' => "one here\nand another here\n"
}

Null

You can use the tilde ‘~’ character for a null value.

1
2
3
name: Mr. Show 
hosted by: Bob and David
date of next season: ~

Ruby code:

1
2
3
4
5
{ 
'name' => 'Mr. Show',
'hosted by' => 'Bob and David',
'date of next season' => nil
}

YAML For Ruby

Symbols

Ruby Symbols can be simply serialized using the !ruby/symbol transfer method, or the abbreviated !ruby/sym.

1
2
3
4
5
6
7
8
simple symbol: !ruby/symbol Simple 
shortcut syntax: !ruby/sym Simple
symbols in seqs:
- !ruby/symbol ValOne
- !ruby/symbol ValTwo
- !ruby/symbol ValThree
symbols in maps:
- !ruby/symbol MapKey: !ruby/symbol MapValue

Ruby code:

1
2
3
4
5
{ 'simple symbol' => :Simple, 
'shortcut syntax' => :Simple,
'symbols in seqs' => [ :ValOne, :ValTwo, :ValThree ],
'symbols in maps' => [ { :MapKey => :MapValue } ]
}

Ranges

Ranges are serialized with the !ruby/range type family.

1
2
3
4
5
normal range: !ruby/range 10..20 
exclusive range: !ruby/range 11...20
negative range: !ruby/range -1..-5
? !ruby/range 0..40
: range as a map key

Ruby code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{ 'normal range' => (10..20), 
'exclusive range' => (11...20),
'negative range' => (-1..-5),
(0..40) => 'range as a map key'
}

### Regexps

Regexps may be serialized to YAML, both its syntax and any modifiers.

```yaml
case-insensitive: !ruby/regexp "/George McFly/i"
complex: !ruby/regexp "/\\A\"((?:[^\"]|\\\")+)\"/"
simple: !ruby/regexp '/a.b/'

Ruby code:

1
2
{ 'simple' => /a.b/, 'complex' => /\A"((?:[^"]|\")+)"/, 
'case-insensitive' => /George McFly/i }

Perl Regexps

Regexps may also be imported from serialized Perl.

1
2
3
--- !perl/regexp: 
REGEXP: "R[Uu][Bb][Yy]$"
MODIFIERS: i

Ruby code:

1
/R[Uu][Bb][Yy]$/i 

Struct class

The Ruby Struct class is registered as a YAML builtin type through Ruby, so it can safely be serialized. To use it, first make sure you define your Struct with Struct::new. Then, you are able to serialize with Struct#to_yaml and unserialize from a YAML stream.

1
2
3
4
5
--- !ruby/struct:BookStruct 
author: Yukihiro Matsumoto
title: Ruby in a Nutshell
year: 2002
isbn: 0-596-00214-9

Ruby code:

1
2
book_struct = Struct::new( "BookStruct", :author, :title, :year, :isbn ) 
book_struct.new( "Yukihiro Matsumoto", "Ruby in a Nutshell", 2002, "0-596-00214-9" )

Nested Structs

As with other YAML builtins, you may nest the Struct inside of other Structs or other data types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- !ruby/struct:FoodStruct 
name: Nachos
ingredients:
- Mission Chips
- !ruby/struct:FoodStruct
name: Tostitos Nacho Cheese
ingredients:
- Milk and Enzymes
- Jack Cheese
- Some Volatile Chemicals
taste: Angelic
- Sour Cream
taste: Zesty
- !ruby/struct:FoodStruct
name: Banana Cream Pie
ingredients:
- Bananas
- Creamy Stuff
- And Such
taste: Puffy

Ruby code:

1
2
3
4
5
6
7
food_struct = Struct::new( "FoodStruct", :name, :ingredients, :taste ) 
[
food_struct.new( 'Nachos', [ 'Mission Chips',
food_struct.new( 'Tostitos Nacho Cheese', [ 'Milk and Enzymes', 'Jack Cheese', 'Some Volatile Chemicals' ], 'Angelic' ),
'Sour Cream' ], 'Zesty' ),
food_struct.new( 'Banana Cream Pie', [ 'Bananas', 'Creamy Stuff', 'And Such' ], 'Puffy' )
]

Objects

YAML has generic support for serializing objects from any class available in Ruby. If using the generic object serialization, no extra code is needed.

1
2
3
--- !ruby/object:YAML::Zoolander 
name: Derek
look: Blue Steel

Ruby code:

1
2
3
4
5
6
7
8
9
10
11
class Zoolander 
attr_accessor :name, :look
def initialize( look )
@name = "Derek"
@look = look
end
def ==( z )
self.name == z.name and self.look == z.look
end
end
Zoolander.new( "Blue Steel" )

Extending Kernel::Array

When extending the Array class, your instances of such a class will dump as YAML sequences, tagged with a class name.

1
2
3
4
--- !ruby/array:YAML::MyArray 
- jacket
- sweater
- windbreaker

Ruby code:

1
2
3
4
5
6
class MyArray < Kernel::Array; end 
outerwear = MyArray.new
outerwear << 'jacket'
outerwear << 'sweater'
outerwear << 'windbreaker'
outerwear

Extending Kernel::Hash

When extending the Hash class, your instances of such a class will dump as YAML maps, tagged with a class name.

1
2
3
4
--- !ruby/hash:YAML::MyHash 
Black Francis: Frank Black
Kim Deal: Breeders
Joey Santiago: Martinis

Ruby code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Note that the @me attribute isn't dumped 
# because the default to_yaml is trained
# to dump as a regular Hash.
class MyHash < Kernel::Hash
attr_accessor :me
def initialize
@me = "Why"
end
end
pixies = MyHash.new
pixies['Black Francis'] = 'Frank Black'
pixies['Kim Deal'] = 'Breeders'
pixies['Joey Santiago'] = 'Martinis'
pixies

Reuse config when possible

extends

extends is a great way to reuse some YAML config in multiple places, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.image_template:
image:
name: centos:latest

test:
extends: .image_template
script:
- echo "Testing"

deploy:
extends: .image_template
script:
- echo "Deploying"
```

YAML has a handy feature called anchors, which lets you easily duplicate content across your document. Anchors can be used to duplicate/inherit properties, and is a perfect example to be used with hidden jobs to provide templates for your jobs. When there is duplicate keys, GitLab will perform a reverse deep merge based on the keys.

```yaml
.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition'
image: ruby:2.6
services:
- postgres
- redis

test1:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test1 project

test2:
<<: *job_definition # Merge the contents of the 'job_definition' alias
script:
- test2 project

Dynamic Code

1
2
3
4
5
6
7
8
---
development:
dialect: postgres
database: postgres
user: <%= ENV["DEVELOPMENT_DATABASE_USER"] %>
password: <%= ENV["DEVELOPMENT_DATABASE_PASSWORD"] %>
host: 127.0.0.1
pool: 5

References

[1] YAML.rb is YAML for Ruby | Cookbook - https://yaml.org/YAML_for_ruby.html

[2] 3 YAML tips for better pipelines | GitLab - https://about.gitlab.com/blog/2020/10/01/three-yaml-tips-better-pipelines/

[3] yaml - rails 3, how use an ENV config vars in a Settings.yml file? - Stack Overflow - https://stackoverflow.com/questions/5866015/rails-3-how-use-an-env-config-vars-in-a-settings-yml-file

[4] GitLab CI/CD pipeline configuration reference | GitLab - - https://docs.gitlab.com/ce/ci/yaml/#anchors

[5] YAML Ain’t Markup Language (YAML™) Version 1.2 - https://yaml.org/spec/1.2/spec.html

[6] The Official YAML Web Site - https://yaml.org/