A General Data Structure (GDS) is a universal, composable data structure and is used to store any kind of data.
Typical usage is the definition of configurations, specifications and data sets.

nice picture

Introduction

In this special definition a General Data Structure (GDS) is either a hash or an array or any composition of nested hashes and arrays. A GDS is used to store any kind of structured data.

The defined data can be used in the following situations:

  • for the configuration or specification of systems, processes, services, frameworks, objects, methods or functions
  • for seeding a database or any other data model
  • to convert to other data representation formats like JSON, XML, YAML

A Hash and an Array

In Ruby you have to use curly braces to define hashes and to group nested hashes. In Ruby you also have to use square brackets to define arrays and to group nested arrays.

A Hash:

A hash is a collection of unique keys and their values - it is a collection of key-value pairs.

h = { key1: 'value a', key2: 'value b' }

An Array:

An array is an ordered collection of any object.

a = [ 1, 2, 3, 4 ]

The GDS Language

The GDS language is a special DSL (domain specific language) for defining general data structures.
It uses a succinct, indentation-sensitive syntax which makes data representation clear and readable.
The building blocks for general data structures are hashes and arrays.

An Example Using the GDS Language

,  
  : name Karl
    addresses ,
      : street Ringstr. 5     |  zipcode 77777  |  city Saarbrücken
      : street Zwirbelweg 8   |  zipcode 61112  |  city Stuttgart

transforms to

[
  { name: 'Karl',
    addresses: [ { street: 'Ringstr. 5'  , zipcode: 77777, city: 'Saarbrücken' },
                 { street: 'Zwirbelweg 8', zipcode: 61112, city: 'Stuttgart'   }  ]
  }
]

Motivation and Features

  • the focus is on the essential data
  • a language for humans, a human-writable format
  • structure and hierarchy is expressed using indentation (whitespace-sensitive) - avoids curly braces and square brackets
  • uses a succinct and minimalist syntax - tries to avoid unnecessary characters - minimizes visual noise
  • in many cases colons, commas and quotes are not necessary
  • less text makes data clearer and more readable
  • for configuration and specification - replacement for XML, JSON, YAML
  • for data/database seeding
  • less text and less potential errors using schema definitions
  • supports block comments, which could be nested
  • allows also the definition of hash and array structures in a kind of restricted classic Ruby like syntax
  • provides an alternative for Ruby hash and array definition without using eval(); can be used as a protection against code injection vulnerabilities, e.g. on web servers

Another Example

The next example demonstrates the definition of a nested hash.
To simplify the definition of a top-level hash structure, you can omit the first : (colon) for the definition of the main hash. This makes your definition more pleasant to read.

caption foo
credit  bar
images
  small
    url  http://mywebsite.com/image-small.jpg
    dimensions
      height 500
      width  500
  large
    url  http://mywebsite.com/image-large.jpg
    dimensions
      height 500
      width  500
videos
  small
    preview  http://mywebsite.com/video.m4v
    dimensions
      height 300
      width  400

transforms to

{
  :caption => 'foo',
  :credit => 'bar',
  :images => {
    :small => {
      :url => 'http://mywebsite.com/image-small.jpg',
      :dimensions => {
        :height => 500,
        :width => 500
      }
    },
    :large => {
      :url => 'http://mywebsite.com/image-large.jpg',
      :dimensions => {
        :height => 500,
        :width => 500
      }
    },
  },
  :videos => {
    :small => {
      :preview => 'http://mywebsite.com/video.m4v',
      :dimensions => {
        :height => 300,
        :width => 400
      }
    }
  }
}

Advanced Example

This example is using a schema specifier.

@schema person(firstname,lastname,age)
, @schema person
  : Harry | Langemann | 44
  : Susi  | Heimstett | 32
  : Bob   | Meiermann | 57

transforms to

[
  { firstname: 'Harry', lastname: 'Langemann', age: 44 },
  { firstname: 'Susi', lastname: 'Heimstett', age: 32 },
  { firstname: 'Bob', lastname: 'Meiermann', age: 57 },
]

Alternative definition with GDS without using schema specifiers:

,
  :
    firstname Harry
    lastname  Langemann
    age       44
  :
    firstname Susi
    lastname  Heimstett
    age       32
  :
    firstname Bob
    lastname  Meiermann
    age       57

Another alternative definition with GDS:

,
  : firstname Harry | lastname Langemann | age 44
  : firstname Susi  | lastname Heimstett | age 32
  : firstname Bob   | lastname Meiermann | age 57

Usage with Ruby

Installation

gem install gdstruct

Coding

h = { a: 'val a', b: 'val b' }

can be coded as

require 'gdstruct'

h = GDstruct.c( <<-EOS )
a val a
b val b
EOS

and

a = [ 1, 2, 3, 4 ]

can be coded as

require 'gdstruct'

a = GDstruct.c( <<-EOS )
,
  1
  2
  3
  4
EOS

Use the class GDstruct and the class method c (alias for create). This method expects a string with the description. In the example the string is defined as a here document.

Syntax Definition

The GDS language uses two basic symbols (: and ,) for the creation of hashes and arrays.

: A colon is used to define a hash.
, A comma is used to define an array.

Use indentation with two spaces for the definition of elements and for nested structures. Tab characters are not allowed for indentation.

Definition of a hash

: Use a colon to define a hash.

Usually each element of a hash needs to be defined on a separate line and needs to be indented one level higher (two spaces more) than the defining ':' (colon) symbol.

The elements of a hash are key-value pairs.
The value part of a key-value pair could be a subhash (a nested hash), an array or a basic value.

:

# => {}
:
  k1 v1
  k2 v2
  k3 v3

# => { k1: 'v1', k2: 'v2', k3: 'v3' }

By default, the key is always converted into a symbol.

You also can already start the definition of key-value pairs at the line of the hash definition with the colon.
The following definition is equivalent to the previous one:

: k1 v1
  k2 v2
  k3 v3

# => { k1: 'v1', k2: 'v2', k3: 'v3' }

Attention: Between the hash defining : (colon) and the following key (in this case k1) there needs to be at least one space character!
This is always the case if the : (colon) for defining a hash and the following first key of the hash are on the same line.

Nested hashes:
You can define a nested hash, if you specify on a line only a key and no value. The key-value pairs of this nested hash have to be defined on new lines and have to be indented one level more (two spaces more). If you don’t define elements for a nested hash, then this hash stays empty.

:
  k1

# => { k1: {} }
:
  k1 v1
  k2 v2
  k3
    k31 v31
    k32
    k33
      k331 v331
      k332
        k3321
  k4 v4

# => { k1: 'v1', k2: 'v2', k3: { k31: 'v31', k32: {}, k33: { k331: 'v331', k332: { k3321: {} } } }, k4: 'v4' }

You also can start the definition of a nested hash at the same line as the definition of the containing hash, that means at the same line as the hash definition with the colon (‘:’). The following is valid syntax:

: k1

# => { k1: {} }

Also this is valid syntax:

: k1
    k11 v11

# => { k1: { k11: "v11" } }

The default structure is a hash

If the overall structure of the definition is a hash, then you can omit the first : (colon) for the definition of the main hash.

This means instead of writing

:
  k1 v1
  k2 v2
  k3
    k31 v31
  k4 v4

# => { k1: 'v1', k2: 'v2', k3: { k31: 'v31' }, k4: 'v4' }

You can write it just like this:

k1 v1
k2 v2
k3
  k31 v31
k4 v4

Please note that the default structure of GDS is a hash.

Multiple key-value pairs on a single line

| Use a vertical bar symbol to separate multiple key-value pairs on a single line.

There is a more compact notation for the definition of key-value pairs supported. You can define multiple key-value pairs on one single line. The vertical bar symbol (|) is used to separate the individual key-value pairs.
This notation is only possible if the value part of the key-value pair is a basic value like a string, an integer, …

:
  k1 v1 | k2 v2 | k3 v3

# =>  { k1: 'v1', k2: 'v2', k3: 'v3' }

The following definition is equivalent to the previous one:

: k1 v1 | k2 v2 | k3 v3

# =>  { k1: 'v1', k2: 'v2', k3: 'v3' }

One more example:

k1 v1 | k2 v2
k3 v3
k4 v4
k5 v5 | k6 v6
k7 v7 | k8 v8 | k9 v9

# =>  { k1: 'v1', k2: 'v2', k3: 'v3', k4: 'v4', k5: 'v5', k6: 'v6', k7: 'v7', k8: 'v8', k9: 'v9' }

Definition of keys

The standard syntax for the key is like for an identifier in most programming languages. The first character needs to be a letter or an underscore ([A-Za-z_]) and can be followed by a sequence of letters, underscores and digits ([A-Za-z0-9_]).
If you would like to use any other characters inside a key, then you need to quote the key with single quotes or double quotes.

As already mentioned, the key is always converted into a symbol.

Please take a look at the following examples:

k1                     v1
'k2'                   v2
"k3"                   v3  
" "                    empty
":"                    :colon
'$'                    dollar sign
' a really long key '  "  very special "
'& %$$! §'             something cryptic

# => { :k1=>"v1", :k2=>"v2", :k3=>"v3", :" "=>"empty", :":"=>:colon, :"$"=>"dollar sign", :" a really long key "=>"  very special ", :"& %$$! §"=>"something cryptic" }

Definition of an array

, Use a comma to define an array.

You can nest an array structure as deep as you like. The following definitions are all valid:

,

# => []
,
  ,

# => [ [] ]
,
  ,
    ,

# => [ [ [] ] ]
,
  ,
    ,
      ,

# => [ [ [ [] ] ] ]

Usually each element of an array needs to be defined on a separate line and needs to be indented one level higher (two spaces more) than the defining ',' (comma) symbol.

,
  v1
  v2
  ,
    v31
    ,
    ,
    v32
    ,
      v331
      v332

# =>  [ 'v1', 'v2', [ 'v31', [], [], 'v32', [ 'v331', 'v332' ] ] ]

Multiple values on a single line

| Use a vertical bar symbol to separate multiple values on a single line.

There is a more compact notation for the definition of values supported. You can define multiple values of an array on one single line. The vertical bar symbol (|) is used to separate the individual values.
This notation is only possible if the values are basic value like a string, an integer, …

,
  v1 | v2 | v3

# =>  [ 'v1', 'v2', 'v3' ]
,
  v1 | v2 | v3
  v4 | v5
  v6

# =>  [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]
, v1 | v2 | v3
  v4 | v5
  v6

# =>  [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]

Definition of a hash containing an array

<key> , define an array within a hash, the array is the value for the <key>.
k1 v1
k2 ,
  v21
  v22
  v23
k3 v3

# =>  { k1: 'v1', k2: [ 'v21', 'v22', 'v23' ], k3: 'v3' }

Definition of an array containing a hash

Array containing hashes with single key-value pair

: <key> <value> define a hash with a single key-value pair
,
  : k1 v1
  : k2 v2

# =>  [ { k1: 'v1' }, { k2: 'v2' } ]

Array containing hashes with multiple key-value pairs

: <newline> define a hash
<indentation +2> <key> <value> with multiple key-value pairs
 
,
  :
    k11 v11
    k12 v12
  :
    k21 v21
    k22 v22

# =>  [ { k11: 'v11', k12: 'v12' }, { k21: 'v21', k22: 'v22' } ]

If you don’t increment the indentation after the colons, then the following would be recognized:

,
  :
  k11 v11
  k12 v12
  :
  k21 v21
  k22 v22

# =>  [ {}, 'k11 v11', 'k12 v12', {}, 'k21 v21', 'k22 v22' ]

General example

,
  v1
  : k2 v2
  : k31 v31 | k32 v32 | k33 v33
    k34 v34
    k35 v35 | k36 v36
  :
    k41 v41
    k42 v42
    k43,
    k44, 1 | 2 | 3
      4 | 5
      6
      7
      8 | 9
      : k441 v441
  k5 :
    k51 v51
    k52 v52      
  :
    k6
      k61  v61        
  v7

# =>  [ 'v1', { k2: 'v2' }, { k31: 'v31', k32: 'v32', k33: 'v33', k34: 'v34', k35: 'v35', k36: 'v36' }, 
        { k41: 'v41', k42: 'v42', k43: [], k44: [1, 2, 3, 4, 5, 6, 7, 8, 9, { k441: 'v441' } ] }, 
        { k5: { k51: 'v51', k52: 'v52' } }, { k6: { k61: 'v61' } }, 'v7' ]

Basic Values

Basic values are integer numbers, floating-point numbers, symbols (Ruby symbols), strings and special keyword values like nil, true and false.

Basic values are used inside the definition of hashes and arrays.
In hashes the value part of a key-value pair could be a subhash (nested hash), an array or a basic value.
In arrays the values could be subarrays (nested arrays), hashes or basic values.

The following literals are available as notations for basic values:

Integer Literals

Integer values are defined like the Ruby syntax.

Take a look at the following examples.

Decimal representation:

  • 1
  • -1
  • +1
  • 0d1
  • -0d1
  • +0d1

Hexadecimal representation:

  • 0xff
  • 0x1001
  • -0xBB

Binary representation:

  • 0b0
  • 0b1
  • 0b00
  • 0b11
  • 0b0_0
  • 0b0_0_0
  • 0b1_0
  • 0b1_0_0

Octal representation:

  • 0o0
  • 00
  • 0o27
  • 027

Floating-Point Literals

Floating-point values are defined like the Ruby syntax.

Examples

  • 1.0
  • -1.0
  • +1.0
  • 1.0e2
  • 1.0E2
  • 1.0e-3
  • 1_000.000_000_1

In contrast to Ruby the following syntax is also valid:

  • .1
  • -.2
  • +.3

Keyword Literals

Keyword literals are specific for the underlying programming language, in this case Ruby.
In the GDS language keyword literals are prefixed with an exclamation mark (!).

The following keyword literals are supported.

Literal Representation in Ruby
!true true
!false false
!nil nil

Ruby Symbols

You can also use Ruby symbols as a basic value.

The literal has to be prefixed with a colon (:) which is followed by a sequence of arbitrary characters except any space characters.

As an example take a look at the following list of symbols:

, 
  :a
  :longerstring
  :longer_string
  :@
  :$
  :$;
  :$,
  :$,,
  :π
  :'
  :''
  :"
  :""
  ::
  :::
  ::::

# =>  [ :a, :longerstring, :longer_string, :"@", :"$", :$;, :$,, :"$,,", :π, :"'", :"''", :"\"", :"\"\"", :":", :"::", :":::" ]

You can use symbols also inside a hash as the value part of a key-value pair.

k
  k1 :asymbol
  k2 :$;
  k3 :@
  k4 :$,,
  k5 ::::

# =>  { k: { k1: :asymbol, :k2=>:$;, k3: :"@", k4: :"$,,", k5: :":::" } }

String Literals

You can use single quotes or double quotes to express string literals.

Examples

  • ‘this is a string’
  • “this is a string”
Escaping

Single-quoted strings use the following escaping

\’

Double-quoted strings use the following escaping

\”

The following escaping is used for both, single- and double-quoted strings

\n line feed character, newline character

Default String Literals

In case the value is not recognized as an integer, a floating-point, a keyword, a symbol or a quoted string, then as a default it will be a string.

The definition of the string lasts until the end of the line or until a vertical bar character (|) appears or until a comment starts. The string will not include any leading or trailing space characters. However the string will contain inner space characters.

,
  mystring
  this is a really long string
  first string : (1)   |   second string : (2)    /*comment*/    |   third string : (3)      # comment

# =>  [ "mystring", "this is a really long string", "first string : (1)", "second string : (2)", "third string : (3)" ]

Comments

The GDS language supports inline comments and block comments.

Inline Comments

Inline comments use the Ruby style inline comment with an beginning # character.

Inline comments can be used after some valid input, e.g. after the definition of a value, at the end of a line. If inline comments are used on its own on a line, without any other valid input, then they can be indented with any number of spaces. The GDS syntax is not enforcing proper indentation for lines containing only inline comments. This is in contrast to some other indentation-sensitive languages, like for example HAML, and makes the usage of comments more handy.

,
  v1 # inline comment at the end of a line

# inline comment with 0 space indentation  
 # inline comment with 1 space indentation  
  # inline comment with 2 space indentation  
   # inline comment with 3 space indentation  
    # inline comment with 4 space indentation  
      # ...  

  v2

# =>  [ 'v1', 'v2' ]

Block Comments

Block comments use the C style block comment syntax with opening /* characters and closing */ characters.
Block comments offer greater flexibility compared to Ruby’s multi-line comments (=begin, =end).
Block comments in GDS can be nested.

You can use block comments before, after or around values or key-value pairs. But please pay attention with block comments before a value (array) or a key-value pair (hash): You have to respect the proper indentation level! Otherwise either the value or key-value pair would be nested wrongly or a syntax error would occure.

If block comments are used on its own on a line, without any other valid input, then they can be indented with any number of spaces. The GDS syntax is not enforcing proper indentation for lines containing only block comments.

:
  k1 v1
  /* bc (block comment), attention: respect proper indentation of this line */ k2 /*bc*/ v2 /*bc*/

/* block comment with 0 space indentation */  
 /* block comment with 1 space indentation */  
  /* block comment with 2 space indentation */
   /* block comment with 3 space indentation */
    /* block comment with 4 space indentation */
     /* ... */

/****
         multi-line block comment
****/  

  /*bc (respect indentation)*/ k3 /*bc*/ , /*bc*/
    /*bc (respect indentation)*/ 1 /*bc*/
    /*bc (respect indentation) /*nested bc*/ */ /*bc*/ 2 /*bc*/ | /*bc*/ 3 /*bc*/

  /*bc (respect indentation)*/ k4 /*bc*/ , /*bc*/ 1 /*bc*/ | /*bc*/ 2 /*bc*/

# =>  { k1: 'v1', k2: 'v2', k3: [1,2,3], k4: [1,2] }

The following definitions all contain wrong indentations for lines with a beginning block comment and a definition of a value after that. All of these definitions would result in a syntax error. Effectively it is the same as using a wrong indentation for the definition of a value without a beginning block comment.

,
  v1
   /*bc !!! wrong indentation !!! */ v2
,
  v1
 /*bc !!! wrong indentation !!! */ v2
,
  v1
    /*bc !!! wrong indentation !!! */ v2

Variables

You can define a variable as a placeholder for a basic value: for an integer, a floating-point, a keyword literal, a symbol or a string.

A variable name is an identifier (unique name) and has to be prefixed with $. Variables are defined at the beginning of a definition of a GDS construct, before any hash or array structures are defined.

$integer   = 10
$float     = 20.3
$symbol    = :sym$%&
$string    = this is a default string
$string_sq = 'single-quoted string'
$string_dq = "double-quoted string"
$flag      = !true
$something = !nil

k1 $integer
k2 $float
k3 $symbol
k4 $string
k5 $string_sq
k6 $string_dq
k7 $flag
k8 $something
valuelist ,
  $integer
  $float
  $symbol
  $string
  $string_sq
  $string_dq
  $flag
  $something

# =>  { k1: 10, k2: 20.3, k3: :"sym$%&", k4: "this is a default string", k5: "single-quoted string", 
#       k6: "double-quoted string", k7: true, k8: nil, 
#       valuelist: [ 10, 20.3, :"sym$%&", "this is a default string", "single-quoted string", "double-quoted string", true, nil ] }

If a variable is used which was not defined before, then an exception is raised.

String Interpolation

In conjunction with variables, there is string interpolation supported.
The values of variables are substituted into string literals.

$object = house
setup "This is a $(object)."

# => { setup: "This is a house." }

String interpolation takes place for double-quoted strings and for default strings, however not for single-quoted strings.
You can prevent string interpolation taking place by escaping the $( sequence with a backslash \.

$errorCode = 301
$errorText = Moved Permanently  
messages
  response1 "The error was: $(errorText); The error code is $(errorCode)."
  response2 The error was: $(errorText); The error code is $(errorCode).
  response3 'The error was: $(errorText); The error code is $(errorCode).'   # no string interpolation in single-quoted string
  response4 The error was: \$(errorText); The error code is $(errorCode).    # prevent string interpolation by escaping with backslash \

# => { messages: {
#        response1: "The error was: Moved Permanently; The error code is 301.", 
#        response2: "The error was: Moved Permanently; The error code is 301.", 
#        response3: "The error was: $(errorText); The error code is $(errorCode).",
#        response4: "The error was: $(errorText); The error code is 301." } }    

During string interpolation the values of variables are first converted to strings and then they are substituted.

$integer = 10
$float   = 20.3
$symbol  = :$hello$
$true    = !true
$false   = !false
$nil     = !nil
  
values ,
  The integer number is $(integer)
  The float number is $(float)
  The symbol is $(symbol)
  True is $(true)
  False is $(false)
  And nil really is nothing: $(nil)

# => { values: [
#        "The integer number is 10",
#        "The float number is 20.3",
#        "The symbol is $hello$",
#        "True is true",
#        "False is false",
#        "And nil really is nothing: " ] }

One more example

$main_path    = /usr/john
$service_dir  = docker-compose-service01
$app01_dir    = application_001
$app02_dir    = application_002
$app03_dir    = application_003
$service_path = $(main_path)/$(service_dir)

config
  app01_path  $(main_path)/$(service_dir)/$(app01_dir)
  app02_path  $(main_path)/$(service_dir)/$(app02_dir)
  app03_path  $(service_path)/$(app03_dir)

# => { config: {
#        app01_path: "/usr/john/docker-compose-service01/application_001", 
#        app02_path: "/usr/john/docker-compose-service01/application_002", 
#        app03_path: "/usr/john/docker-compose-service01/application_003"  } }

Schema Specifiers

Schema specifiers can be used to predefine the keys of a hash or subhash. When you specify the values you no longer need to name the key for each value. This facilitates the input for hashes and prevents typos for keys.
In combination with the vertical bar symbol (|), which allows to define multiple values on a single line, this features a slim and clearly arranged, table-like style for data.

@schema country(name,capital,area,population,vehicleRegistrationCode,iso3166code,callingCode)
, @schema country                                                                                                              /*
  ----------------------------------------------------------------------------------------------------------------------------
    name            capital            area (km^2)   population      vehicleRegistrationCode   iso3166code   callingCode
  ---------------------------------------------------------------------------------------------------------------------------- */
  : Deutschland   | Berlin           |    357_385  |    82_521_653 | D                       | DE          | 49
  : USA           | Washington, D.C. |  9_833_520  |   325_719_178 | USA                     | US          | 1
  : China         | Beijing          |  9_596_961  | 1_403_500_365 | CHN                     | CN          | 86
  : India         | New Dehli        |  3_287_469  | 1_339_180_000 | IND                     | IN          | 91
  : Austria       | Vienna           |     83_878  |     8_822_267 | A                       | AT          | 43
  : Denmark       | Copenhagen       |     42_921  |     5_748_769 | DK                      | DK          | 45
  : Canada        | Ottawa           |  9_984_670  |    36_503_097 | CDN                     | CA          | 1
  : France        | Paris            |    643_801  |    66_991_000 | F                       | FR          | 33
  : Russia        | Moscow           | 17_075_400  |   144_526_636 | RUS                     | RU          | 7

transforms to

[
  { :name=>"Deutschland", :capital=>"Berlin", :area=>357385, :population=>82521653, :vehicleRegistrationCode=>"D", :iso3166code=>"DE", :callingCode=>49 },
  { :name=>"USA", :capital=>"Washington, D.C.", :area=>9833520, :population=>325719178, :vehicleRegistrationCode=>"USA", :iso3166code=>"US", :callingCode=>1 },
  { :name=>"China", :capital=>"Beijing", :area=>9596961, :population=>1403500365, :vehicleRegistrationCode=>"CHN", :iso3166code=>"CN", :callingCode=>86 },
  { :name=>"India", :capital=>"New Dehli", :area=>3287469, :population=>1339180000, :vehicleRegistrationCode=>"IND", :iso3166code=>"IN", :callingCode=>91 },
  { :name=>"Austria", :capital=>"Vienna", :area=>83878, :population=>8822267, :vehicleRegistrationCode=>"A", :iso3166code=>"AT", :callingCode=>43 },
  { :name=>"Denmark", :capital=>"Copenhagen", :area=>42921, :population=>5748769, :vehicleRegistrationCode=>"DK", :iso3166code=>"DK", :callingCode=>45 },
  { :name=>"Canada", :capital=>"Ottawa", :area=>9984670, :population=>36503097, :vehicleRegistrationCode=>"CDN", :iso3166code=>"CA", :callingCode=>1 },
  { :name=>"France", :capital=>"Paris", :area=>643801, :population=>66991000, :vehicleRegistrationCode=>"F", :iso3166code=>"FR", :callingCode=>33 },
  { :name=>"Russia", :capital=>"Moscow", :area=>17075400, :population=>144526636, :vehicleRegistrationCode=>"RUS", :iso3166code=>"RU", :callingCode=>7 }
]

which could be pretty written in classic Ruby syntax like

[
  { name: "Deutschland", capital: "Berlin"          , area:    357_385, population:    82_521_653, vehicleRegistrationCode: "D"  , iso3166code: "DE", callingCode: 49 },
  { name: "USA"        , capital: "Washington, D.C.", area:  9_833_520, population:   325_719_178, vehicleRegistrationCode: "USA", iso3166code: "US", callingCode: 1  },
  { name: "China"      , capital: "Beijing"         , area:  9_596_961, population: 1_403_500_365, vehicleRegistrationCode: "CHN", iso3166code: "CN", callingCode: 86 },
  { name: "India"      , capital: "New Dehli"       , area:  3_287_469, population: 1_339_180_000, vehicleRegistrationCode: "IND", iso3166code: "IN", callingCode: 91 },
  { name: "Austria"    , capital: "Vienna"          , area:     83_878, population:     8_822_267, vehicleRegistrationCode: "A"  , iso3166code: "AT", callingCode: 43 },
  { name: "Denmark"    , capital: "Copenhagen"      , area:     42_921, population:     5_748_769, vehicleRegistrationCode: "DK" , iso3166code: "DK", callingCode: 45 },
  { name: "Canada"     , capital: "Ottawa"          , area:  9_984_670, population:    36_503_097, vehicleRegistrationCode: "CDN", iso3166code: "CA", callingCode: 1  },
  { name: "France"     , capital: "Paris"           , area:    643_801, population:    66_991_000, vehicleRegistrationCode: "F"  , iso3166code: "FR", callingCode: 33 },
  { name: "Russia"     , capital: "Moscow"          , area: 17_075_400, population:   144_526_636, vehicleRegistrationCode: "RUS", iso3166code: "RU", callingCode: 7  }
]

which could be pretty written in GDS style with no schema specifier like

,
  : name Deutschland   | capital Berlin           | area    357_385  | population    82_521_653 | vehicleRegistrationCode D    | iso3166code DE | callingCode 49
  : name USA           | capital Washington, D.C. | area  9_833_520  | population   325_719_178 | vehicleRegistrationCode USA  | iso3166code US | callingCode 1
  : name China         | capital Beijing          | area  9_596_961  | population 1_403_500_365 | vehicleRegistrationCode CHN  | iso3166code CN | callingCode 86
  : name India         | capital New Dehli        | area  3_287_469  | population 1_339_180_000 | vehicleRegistrationCode IND  | iso3166code IN | callingCode 91
  : name Austria       | capital Vienna           | area     83_878  | population     8_822_267 | vehicleRegistrationCode A    | iso3166code AT | callingCode 43
  : name Denmark       | capital Copenhagen       | area     42_921  | population     5_748_769 | vehicleRegistrationCode DK   | iso3166code DK | callingCode 45
  : name Canada        | capital Ottawa           | area  9_984_670  | population    36_503_097 | vehicleRegistrationCode CDN  | iso3166code CA | callingCode 1
  : name France        | capital Paris            | area    643_801  | population    66_991_000 | vehicleRegistrationCode F    | iso3166code FR | callingCode 33
  : name Russia        | capital Moscow           | area 17_075_400  | population   144_526_636 | vehicleRegistrationCode RUS  | iso3166code RU | callingCode 7

As you see, the lines are getting very long and there is a lot of text to read. Clarity declines.

Schema specifiers are defined at the beginning of a definition of a GDS construct, before any hash or array structures are defined. You use the directive @schema, an identifier (unique name) for the schema and, within parentheses, a comma-separated list of key names.

@schema schema1(key1,key2,key3)

Schemas definitions are used for the construction of an array of hashes. You use a schema definition by using the @schema directive again and naming the schema specifier. This has to be located after the comma symbol (,) for the definition of an array. This can happen in two cases:

  • for the definition of a top-level array or a nested array
# top-level array
, @schema schema1
  : val1_key1  |  val1_key2  |  val1_key3
  : val2_key1  |  val2_key2  |  val2_key3
  ...

# nested array
,
  , @schema schema1
    : val1_key1  |  val1_key2  |  val1_key3
    : val2_key1  |  val2_key2  |  val2_key3
    ...
  • for the definition of an array which is related to a hash key inside the definition of a hash
:
  key , @schema schema1
    : val1_key1  |  val1_key2  |  val1_key3
    : val2_key1  |  val2_key2  |  val2_key3
    ...

As the schema defines the layout or structure of a hash, each line defining the values has to start with the colon symbol (:) for the definition of a hash.

If a schema specifier is used which was not defined before, then an exception is raised.

Extending schema definitions with additional data

Each hash created with a schema definition can be extended with additional information, that means with additional key-value pairs.
This keeps data definition flexible.
Please note, the definition of additional key-value pairs needs to start in a new line.

@schema person(firstname,lastname,age)
, @schema person
  : Harry | Langemann | 44
    hobby bicycling                                 # additional data
    favoriteFood Spaghetti | favoriteColor blue     # additional data
  : Susi  | Heimstett | 32
  : Bob   | Meiermann | 57

transforms to

[
  { firstname: 'Harry', lastname: 'Langemann', age: 44, hobby: 'bicycling', favoriteFood: 'Spaghetti', favoriteColor: 'blue' },
  { firstname: 'Susi', lastname: 'Heimstett', age: 32 },
  { firstname: 'Bob', lastname: 'Meiermann', age: 57 }
]

Nested schema definitions

An example with two schema specifiers, where one is used nested to the other:

@schema p(firstname, lastname, phone1, phone2, email, web)  # person
@schema a(street, zipcode, city, country)                   # address
, @schema p
  : May         |  Grimes       |  '1-916-595-1175'   |  '(690) 557-4123'   |  may@connelly.name      |  https://www.ryanwolff.net
    addresses , @schema a
      : Grayce Mall 4833           |  77071  |  East Rebeka           |  Cocos (Keeling) Islands
      : Abshire Turnpike 297       |  94595  |  Dasiaberg             |  Cocos (Keeling) Islands
      : Ortiz Shores 29441         |  56813  |  Windlerbury           |  Anguilla             
      : Legros Village 621         |  42101  |  North Flavio          |  Cape Verde

  : Jammie      |  Beahan       |  '(717) 922-5565'   |  '(345) 161-3052'   |  jammie@dicki.biz       |  https://www.corkerylind.org
    addresses , @schema a
      : Rosalyn Ports 2019        |  35316  |  East Willachester     |  Democratic People's Republic of Korea
      : Rosario Locks 56923       |  11183  |  North Marionbury      |  Mozambique           

  : Mariane     |  Roob         |  '(749) 095-8597'   |  '(783) 183-1409'   |  mariane@fay.com        |  https://www.bosco.co     
    addresses , @schema a
      : Dickens Pine 89210        |  24325  |  Lake Alessandraton    |  Nicaragua            
      : Kamren Corners 742        |  98862  |  Brionnamouth          |  Guadeloupe           
      : Erik Branch 67028         |  56464  |  Kertzmannstad         |  Kyrgyz Republic      

  : Ansel       |  Braun        |  '(162) 247-8471'   |  '303-108-0535'     |  ansel@heidenreich.name |  https://www.quitzondickens.net
    addresses , @schema a
      : Littel Path 650           |  26250  |  Lorenzoton            |  Saint Helena         

In Ruby syntax it would be like this:

[
  { firstname: "May", lastname: "Grimes", phone1: "1-916-595-1175", phone2: "(690) 557-4123", email: "may@connelly.name", website: "https://www.ryanwolff.net",
    addresses: [
      { street: "Grayce Mall 4833"    , zipcode: 77071, city: "East Rebeka"       , country: "Cocos (Keeling) Islands" },
      { street: "Abshire Turnpike 297", zipcode: 94595, city: "Dasiaberg"         , country: "Cocos (Keeling) Islands" },
      { street: "Ortiz Shores 29441"  , zipcode: 56813, city: "Windlerbury"       , country: "Anguilla" },
      { street: "Legros Village 621"  , zipcode: 42101, city: "North Flavio"      , country: "Cape Verde" }
    ], },
 { firstname: "Jammie", lastname: "Beahan", phone1: "(717) 922-5565", phone2: "(345) 161-3052", email: "jammie@dicki.biz", website: "https://www.corkerylind.org",
   addresses: [
     { street: "Rosalyn Ports 2019"   , zipcode: 35316, city: "East Willachester" , country: "Democratic People's Republic of Korea" },
     { street: "Rosario Locks 56923"  , zipcode: 11183, city: "North Marionbury"  , country: "Mozambique" }
   ], },
 { firstname: "Mariane", lastname: "Roob", phone1: "(749) 095-8597", phone2: "(783) 183-1409", email: "mariane@fay.com", website: "https://www.bosco.co",
   addresses: [
     { street: "Dickens Pine 89210"   , zipcode: 24325, city: "Lake Alessandraton", country: "Nicaragua" },
     { street: "Kamren Corners 742"   , zipcode: 98862, city: "Brionnamouth"      , country: "Guadeloupe" },
     { street: "Erik Branch 67028"    , zipcode: 56464, city: "Kertzmannstad"     , country: "Kyrgyz Republic" }
   ], },
 { firstname: "Ansel", lastname: "Braun", phone1: "(162) 247-8471", phone2: "303-108-0535", email: "ansel@heidenreich.name", website: "https://www.quitzondickens.net",
   addresses: [
     { street: "Littel Path 650"      , zipcode: 26250, city: "Lorenzoton"        , country: "Saint Helena" }
   ], },
]

You have to decide which style to prefer. If you want to explicitly write key-value pairs or if you want to use schema specifiers.
I agree, the last example with nested schema specifiers could be somewhat dizzying.

However, it gets clearer if you use a different layout:

@schema p(firstname, lastname, phone1, phone2, email, website)  # person
@schema a(street, zipcode, city, country)                       # address
, @schema p                                                                                                                                                                                                                                      /*
  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    firstname      lastname    phone1               phone2               email                     website                         street                   zipcode   city                     country
  ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
  : May         |  Grimes   |  '1-916-595-1175'  |  '(690) 557-4123'  |  may@connelly.name      |  https://www.ryanwolff.net
    addresses , @schema a
      :                                                                                                                            Grayce Mall 4833      |  77071  |  East Rebeka           |  Cocos (Keeling) Islands
      :                                                                                                                            Abshire Turnpike 297  |  94595  |  Dasiaberg             |  Cocos (Keeling) Islands
      :                                                                                                                            Ortiz Shores 29441    |  56813  |  Windlerbury           |  Anguilla             
      :                                                                                                                            Legros Village 621    |  42101  |  North Flavio          |  Cape Verde
  : Jammie      |  Beahan   |  '(717) 922-5565'  |  '(345) 161-3052'  |  jammie@dicki.biz       |  https://www.corkerylind.org
    addresses , @schema a
      :                                                                                                                            Rosalyn Ports 2019    |  35316  |  East Willachester     |  Democratic People's Republic of Korea
      :                                                                                                                            Rosario Locks 56923   |  11183  |  North Marionbury      |  Mozambique           
  : Mariane     |  Roob     |  '(749) 095-8597'  |  '(783) 183-1409'  |  mariane@fay.com        |  https://www.bosco.co     
    addresses , @schema a
      :                                                                                                                            Dickens Pine 89210    |  24325  |  Lake Alessandraton    |  Nicaragua            
      :                                                                                                                            Kamren Corners 742    |  98862  |  Brionnamouth          |  Guadeloupe           
      :                                                                                                                            Erik Branch 67028     |  56464  |  Kertzmannstad         |  Kyrgyz Republic      
  : Ansel       |  Braun    |  '(162) 247-8471'  |  '303-108-0535'    |  ansel@heidenreich.name |  https://www.quitzondickens.net
    addresses , @schema a
      :                                                                                                                            Littel Path 650       |  26250  |  Lorenzoton            |  Saint Helena         

Data is not available - @na directive

In the case the value for a corresponding key is not available, you can use the @na directive (not available) to express this circumstance.
The resulting hash does not contain a key-value pair for this key.

@schema person( firstname, lastname, age )
, @schema person
  : Harry | Langemann | 44
  : @na   | Heimstett | 32
  : Bob   | Meiermann | 57

# => [ { firstname: 'Harry', lastname: 'Langemann', age: 44 }, 
#      { lastname: 'Heimstett', age: 32 }, { firstname: 'Bob', lastname: 'Meiermann', age: 57 } ]

Schema specifiers defined at first time use

Instead of defining a named schema specifiers at the very beginning of a GDS definition, you also can define it at the location of its first time use.

persons1 , @schema person( firstname, lastname, age )
  : Harry   | Langemann | 44
  : Susi    | Heimstett | 32
  : Bob     | Meiermann | 57
persons2 , @schema person  
  : Ludwig  | Reinemann | 33
  : Claudia | Schmidt   | 67

# => { persons1: [ { firstname: 'Harry', lastname: 'Langemann', age: 44 }, { firstname: 'Susi', lastname: 'Heimstett', age: 32 },
#                  { firstname: 'Bob', lastname: 'Meiermann', age: 57 } ],
#      persons2: [ { firstname: 'Ludwig', lastname: 'Reinemann', age: 33 }, { firstname: 'Claudia', lastname: 'Schmidt', age: 67 } ] }  

Anonymous schema specifiers

If the schema specifier is used only once, there is no need to define an identifier for it, you can use an anonymous schema specifier.

persons , @schema( firstname, lastname, age )
  : Harry | Langemann | 44
  : Susi  | Heimstett | 32
  : Bob   | Meiermann | 57

# => { persons: [ { firstname: 'Harry', lastname: 'Langemann', age: 44 }, { firstname: 'Susi', lastname: 'Heimstett', age: 32 },
#                 { firstname: 'Bob', lastname: 'Meiermann', age: 57 } ] }  

Edge cases

If there are less values listed than keys for the schema specifier are defined, then for the resulting hash there will only key-value pairs for the available values be created.

, @schema( firstname, lastname, age )
  : Harry | Langemann | 44
  : Susi  
  : Bob   | Meiermann | 57

# => [ { firstname: 'Harry', lastname: 'Langemann', age: 44 }, 
#      { firstname: 'Susi' }, { firstname: 'Bob', lastname: 'Meiermann', age: 57 } ]

Just to mention, using the @na directive would lead to the same result, but is more verbose :

, @schema( firstname, lastname, age )
  : Harry | Langemann | 44
  : Susi  | @na       | @na
  : Bob   | Meiermann | 57

# => [ { firstname: 'Harry', lastname: 'Langemann', age: 44 }, 
#      { firstname: 'Susi' }, { firstname: 'Bob', lastname: 'Meiermann', age: 57 } ]

If there are more values listed than keys for the schema specifier are defined, then an exception is raised.

, @schema( firstname, lastname, age )
  : Harry | Langemann | 44
  : Susi  | Heimstett | 32 | 99
  : Bob   | Meiermann | 57 

# Exception (SyntaxError) is raised
#
# GDS: too many values for schema definition: line 3, column 5
# --->
# , @schema( firstname, lastname, age )
#   : Harry | Langemann | 44
#   : Susi  | Heimstett | 32 | 99
#     ^
# <---

References

References can be used to define an alias for a certain sub structure, that means for a nested array or a nested hash.
Afterwards in the GDS definition, you can use the reference to refer to this sub structure.

For the definition of a reference you prefix an identifier (unique name) with an & ampersand.
For the use of a reference you prefix the same identifier with an * asterisk.

&ref define the reference named ref
*ref use the reference named ref

Let’s start with an example:

&ref config
  param1 value 1 | param2 value 2 
special *ref

# => { config: { param1: "value 1", param2: "value 2" }, special: { config: { param1: "value 1", param2: "value 2" } } }

References can be defined at miscellaneous places. Here are more examples:

config &ref
  param1 value 1 | param2 value 2 
special *ref

# => { config: { param1: "value 1", param2: "value 2" }, special: { param1: "value 1", param2: "value 2" } }
config 
  &ref param1 value 1 | param2 value 2
  param3 value 3 
special *ref

# => { config: { param1: "value 1", param2: "value 2", param3: "value 3" }, special: { param1: "value 1", param2: "value 2" } }
config 
  param1 value 1 | param2 value 2
  &ref param3 value 3 
special *ref

# => { config: { param1: "value 1", param2: "value 2", param3: "value 3" }, special: { param3: "value 3" } }

Examples for references in combination with arrays:

config
  &ref list ,
    1
    2
    3
special *ref

# => { config: { list: [ 1, 2, 3 ] }, special: { list: [ 1, 2, 3 ] } }
config
  list &ref ,
    1
    2
    3
special *ref

# => { config: { list: [ 1, 2, 3 ] }, special: [ 1, 2, 3 ] }
config
  list , &ref 1 | 2
    3
special *ref

# => { config: { list: [ 1, 2, 3 ] }, special: [ 1, 2 ] }
config
  list , &ref 1
    2
    3
special *ref

# => { config: { list: [ 1, 2, 3 ] }, special: [ 1 ] }
config
  list , 1
    &ref 2 | 3
    4
special *ref

# => { config: { list: [ 1, 2, 3, 4 ] }, special: [ 2, 3 ] }

And again more examples:

,
  &ref, 
    1
    2
    3
    4
  *ref

# => [ [ 1, 2, 3, 4 ], [ 1, 2, 3, 4 ] ]
,
  , &ref 1 | 2
    3
    4
    *ref

# => [ [ 1, 2, 3, 4, [ 1, 2 ] ] ]
,
  &ref k : 
    k1 v1
    k2 v2
  2
  3
  *ref

# => [ { k: { k1: "v1", k2: "v2" } }, 2, 3, { k: { k1: "v1", k2: "v2" } } ]
,
  k &ref : 
    k1 v1
    k2 v2
  2
  3
  *ref

# => [ { k: { k1: "v1", k2: "v2" } }, 2, 3, { k1: "v1", k2: "v2" } ]
,
  1
  &ref :
    k1
      k11 v11
    k2
      k21 v21  
  2
  3
  *ref

# => [ 1, { k1: { k11: "v11" }, k2: { k21: "v21" } }, 2, 3, { k1: { k11: "v11" }, k2: { k21: "v21" } } ]
,
  1
  :
    &ref k1
      k11 v11
    k2
      k21 v21  
  2
  3
  *ref

# => [ 1, { k1: { k11: "v11" }, k2: { k21: "v21" } }, 2, 3, { k1: { k11: "v11" } } ]

If a reference is used which was not defined before, then an exception is raised.

Merging a hash into another - @merge

The @merge directive can be used to merge a hash specified with a reference into another hash.

An example similar to the Rails database configuration file config/database.yml

default &default
  adapter sqlite3
  pool    5
  timeout 5000

development
  @merge *default
  database db/development.sqlite3

test
  @merge *default
  database db/test.sqlite3

production
  @merge *default
  database db/production.sqlite3

# => {
#      default: { adapter: 'sqlite3', pool: 5, timeout: 5000 },
#      development: { adapter: 'sqlite3', pool: 5, timeout: 5000, database: 'db/development.sqlite3' },
#      test: { adapter: 'sqlite3', pool: 5, timeout: 5000, database: 'db/test.sqlite3' },
#      production: { adapter: 'sqlite3', pool: 5, timeout: 5000, database: 'db/production.sqlite3' }
#    }

After you have merged a hash, you can overwrite existing hash entries.

$logpath = /var/log/myapp

initial_settings &init
  priority 10
  max_size 2000
  max_age  10

all_log_files ,  
  : @merge *init
    file  $(logpath)/logfile_1.log
  : @merge *init
    file  $(logpath)/logfile_2.log
  : @merge *init
    file     $(logpath)/logfile_3.log
    max_age  8                          # overwrite an existing hash entry

# => { initial_settings: { priority: 10, max_size: 2000, max_age: 10 },
#      all_log_files: [
#        { priority: 10, max_size: 2000, max_age: 10, file: "/var/log/myapp/logfile_1.log" },
#        { priority: 10, max_size: 2000, max_age: 10, file: "/var/log/myapp/logfile_2.log" },
#        { priority: 10, max_size: 2000, max_age: 8, file: "/var/log/myapp/logfile_3.log" } ] }

Inserting an array into another - @insert

The @insert directive can be used to insert an array specified with a reference into another array.

This is an example:

config_files &config_files ,
  Gemfile
  config/database.yml
  config/application.gdstruct
  
docu_files &docu_files ,
  README.md
  CHANGELOG.md
  License.md
  
app_files &app_files ,
  appmodel.rb
  appview.rb
  appcontroller.rb

complete_file_list ,
  @insert *config_files
  @insert *docu_files
  @insert *app_files
  my_special.rb

# => { config_files: [ "Gemfile", "config/database.yml", "config/application.gdstruct" ],
#      docu_files: [ "README.md", "CHANGELOG.md", "License.md" ],
#      app_files: [ "appmodel.rb", "appview.rb", "appcontroller.rb" ],
#      complete_file_list: [ "Gemfile", "config/database.yml", "config/application.gdstruct", "README.md", "CHANGELOG.md", 
#                            "License.md", "appmodel.rb", "appview.rb", "appcontroller.rb", "my_special.rb" ] }

References together with Schema Specifiers

References are often used in the context of schema definitions to realize associations like one-to-one, one-to-many or many-to-many relationships.
The data structures can be defined in one block and easily be stored in a database.

The following example shows a list of bicycles with each of them assigned to a bike category.
There is a one-to-many relationship between category and bike. One category can be used for many bikes. More natural, many bikes can be assigned to one category.

categories , @schema( name )
  &e_bike         : E-Bike
  &mountain_bike  : Mountain Bike
  &city_bike      : City Bike
  &kids_bike      : Kids Bike
  
bikes , @schema( name, frame_size, wheel_diameter, weight, category )                      /*
  ----------------------------------------------------------------------------------------   
    name               frame size ["]   wheel diameter ["]   weight [kg]   category
  ---------------------------------------------------------------------------------------- */  
  : Speedstar I      | 18             | 27.5               | 23.3        | *city_bike
  : Speedstar I      | 20             | 27.5               | 24.4        | *city_bike
  : Speedstar II     | 22             | 27.5               | 25.5        | *city_bike
  : Little Pony      | 16             | 16                 |  5.98       | *kids_bike
  : Rocket           | 20             | 20                 |  7.13       | *kids_bike
  : Easy Cruiser     | 20             | 28                 | 29.4        | *e_bike 
  : Rough Buffalo    | 19             | 26                 | 14.7        | *mountain_bike 

# => { categories: [ { name: "E-Bike" }, { name: "Mountain Bike" }, { name: "City Bike" }, { name: "Kids Bike" } ],
#      bikes: [ { name: "Speedstar I", frame_size: 18, wheel_diameter: 27.5, weight: 23.3, category: { name: "City Bike" } },
#               { name: "Speedstar I", frame_size: 20, wheel_diameter: 27.5, weight: 24.4, category: { name: "City Bike" } },
#               { name: "Speedstar II", frame_size: 22, wheel_diameter: 27.5, weight: 25.5, category: { name: "City Bike" } },
#               { name: "Little Pony", frame_size: 16, wheel_diameter: 16, weight: 5.98, category: { name: "Kids Bike" } },
#               { name: "Rocket", frame_size: 20, wheel_diameter: 20, weight: 7.13, category: { name: "Kids Bike" } },
#               { name: "Easy Cruiser", frame_size: 20, wheel_diameter: 28, weight: 29.4, category: { name: "E-Bike" } }, 
#               { name: "Rough Buffalo", frame_size: 19, wheel_diameter: 26, weight: 14.7, category: { name: "Mountain Bike" } } ] }

Please also take a look in the chapter Practical Examples where several examples for simple Rails applications with their database seeding files are shown.

Classic Ruby Syntax

The GDS language also supports the classic Ruby syntax for the notation of hash and array structures. You are using familiar curly braces and square brackets for structuring.

{ k1: 'v1', k2: 'v2' }  

also this syntax is valid:

{ :k => {:k1=>'v1'} }
[ 1, 1_000, 2.0, 3.0e10, true, false, nil, { key1: 'val', key2: :"a complicated symbol &&%" } ]
[
  1,
  2.0,
  true,
  {
    key1: "val",
    key2: :"a complicated symbol &&%",
    key3: [
      "string1",
      "string2"
    ]
  }
]

There are a couple of peculiarities you have to consider:

  • Keys of a hash always have to be a symbol (Ruby symbol).
  • For values work integer literals, floating-point literals, symbols (Ruby symbols) and string literals as expected.
  • The GDS default string literals can’t be used. You always have to quote string literals explicitly.
  • Keyword literals have to be written like in Ruby, without a leading exclamation mark (!) : true, false, nil
  • Schema specifiers can’t be used.
  • The syntax is not indentation-sensitive.
  • You have to decide right at the start, with the first expression, if you want to use the special, indentation-sensitive GDS language syntax or the classic Ruby syntax. You must not switch to the other syntax later or mix the two.
  • You can use inline comments and block comments.

Why should I use the GDS language to define structures in the classic Ruby syntax?
Let’s say you develop a web application where you want to provide the user the possiblility to input hash/array structures. Reasons for this could be for example to use these definitions as test data, as input for functions to convert into another format or as expected results to verify in unit tests, which can be specified in this fictitious web application. It would not be a good idea to evaluate those expressions with Ruby itself (eval(), instance_eval()), as this would cause code injection vulnerabilities, or more specific dynamic evaluation vulnerabilities. The proper way is to use a separate interpreter to evaluate these untrusted data.

If you don’t like the indentation-sensitive style provided by the GDS syntax, you can use the classic Ruby syntax and still benefit from the protection against code injection vulnerabilities.

Language Extensions

The GDS language provides a couple of special directives. Each directive begins with a @ character.

Accessing environment variables - @env

The @env directive can be used to access environment variables.
You can use it as a special kind of value inside an array or as the value part of a key-value pair inside a hash.

You have to name the environment variable within parentheses.

For configuration files you can supply properties with environment variables.

Environment variables enable you to parameterize configuration files and dynamically pass settings to them, without changing the main configuration file. By separating these settings from the configuration file, you don’t need to update your configuration file when you need to change the configuration based on different settings. This situation frequently happens, when your application is going through different lifecycle stages like development, testing and production. Each stage uses its own environment, for example its own database.

Example configuration:

# configuration file
# file name: /develop/projectconf.gdstruct

project_settings
  superuser    Administrator
  project      @env(MYPROJECT_NAME)
  projectid    @env(MYPROJECT_ID)
  database     @env(MYPROJECT_DATABASE)
  db_adapter   @env(MYPROJECT_DB_ADAPTER)
d = GDstruct.create_from_file( '/develop/projectconf.gdstruct', allow_env: true )

# =>  {:project_settings=>{:superuser=>"Administrator", :project=>"MyProject", :projectid=>"1001", :database=>"specialdb", :db_adapter=>"postgres"}}

This example uses a separate file for the GDS definition. The class method create_from_file (alias c_from_file) interprets the GDS definition.
To prevent security vulnerabilities and to make sure that you use this feature only in a controlled context, you have to set the optional parameter allow_env to true, when you want to use the @env directive to access environment variables. This works the same for the class method create (alias c).

Evaluating embedded Ruby code - @r

The @r directive (r for Ruby) can be used to evaluate embedded Ruby source code.
You can use it as a special kind of value inside an array or as the value part of a key-value pair inside a hash.

You have to note the Ruby code within parentheses.

Example with embedded Ruby code:

class MyClass
  attr_accessor :a, :b
  def initialize
    @a = 80
    @b = 90
  end
end

myobj = MyClass.new

@myhash = { k1: "v1", k2: "v2" }

d = GDstruct.c( <<EOS, context: binding )
,
  @r( (1..10).to_a )
  @r( myobj )           # local variable
  @r( @myhash )         # instance variable
  @r(
    o = MyClass.new
    o.a = 50**2
    o.b = 4000
    o 
  )
  : year @r(Time.now.year)
EOS

# =>  [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], #<MyClass:0x3afc248 @a=80, @b=90>, {:k1=>"v1", :k2=>"v2"}, #<MyClass:0x3a34cb8 @a=2500, @b=4000>, {:year=>2018}]

To prevent security vulnerabilities and to make sure that you use this feature only in a controlled environment, you have to set the optional parameter context to the binding in which you want to execute the Ruby code. This works the same for the class method create_from_file (alias c_from_file).

Limitations

There are several items on the TODO list to make the GDS language more expressive and more flexible.

Please let me know when you hit some limiting borders.

Example: Conversion to JSON

You can use the GDS language to define a data structure in a clear and succinct representation and after that convert it to the JSON data format.

Example:

require 'gdstruct'
require 'json'

res = GDstruct.c( <<-EOS ).to_json
caption foo
credit  bar
images
  small
    url  http://mywebsite.com/image-small.jpg
    dimensions
      height 500
      width  500
  large
    url  http://mywebsite.com/image-large.jpg
    dimensions
      height 500
      width  500
videos
  small
    preview  http://mywebsite.com/video.m4v
    dimensions
      height 300
      width  400
EOS

The JSON representation:

{"caption":"foo","credit":"bar","images":{"small":{"url":"http://mywebsite.com/image-small.jpg","dimensions":{"height":500,"width":500}},"large":{"url":"http://mywebsite.com/image-large.jpg","dimensions":{"height":500,"width":500}}},"videos":{"small":{"preview":"http://mywebsite.com/video.m4v","dimensions":{"height":300,"width":400}}}}

Prettier formated the JSON representation looks the following:

{ "caption": "foo",
  "credit": "bar",
  "images": {
    "small": {
      "url": "http://mywebsite.com/image-small.jpg",
      "dimensions": {
        "height": 500,
        "width": 500 } },
    "large": {
      "url": "http://mywebsite.com/image-large.jpg",
      "dimensions": {
        "height": 500,
        "width": 500 } } },
  "videos": {
    "small": {
      "preview": "http://mywebsite.com/video.m4v",
      "dimensions": {
        "height": 300,
        "width": 400 } } } }

Example: Conversion to XML

You can use the GDS language to define a data structure in a clear and succinct representation and after that convert it to the XML data format.

Example:

require 'gdstruct'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'

res = GDstruct.c( <<-EOS ).to_xml(root: :data)
caption foo
credit  bar
images
  small
    url  http://mywebsite.com/image-small.jpg
    dimensions
      height 500
      width  500
  large
    url  http://mywebsite.com/image-large.jpg
    dimensions
      height 500
      width  500
videos
  small
    preview  http://mywebsite.com/video.m4v
    dimensions
      height 300
      width  400
EOS

The XML representation looks the following:

<?xml version="1.0" encoding="UTF-8"?>
<data>
  <caption>foo</caption>
  <credit>bar</credit>
  <images>
    <small>
      <url>http://mywebsite.com/image-small.jpg</url>
      <dimensions>
        <height type="integer">500</height>
        <width type="integer">500</width>
      </dimensions>
    </small>
    <large>
      <url>http://mywebsite.com/image-large.jpg</url>
      <dimensions>
        <height type="integer">500</height>
        <width type="integer">500</width>
      </dimensions>
    </large>
  </images>
  <videos>
    <small>
      <preview>http://mywebsite.com/video.m4v</preview>
      <dimensions>
        <height type="integer">300</height>
        <width type="integer">400</width>
      </dimensions>
    </small>
  </videos>
</data>

Comparing to YAML

You can use the GDS language to define a data structure in a clear and succinct representation and after that convert it to the YAML data format.

Example:

require 'gdstruct'
require 'yaml'

res = GDstruct.c( <<-EOS ).to_yaml
caption foo
credit  bar
images
  small
    url  http://mywebsite.com/image-small.jpg
    dimensions
      height 500
      width  500
  large
    url  http://mywebsite.com/image-large.jpg
    dimensions
      height 500
      width  500
videos
  small
    preview  http://mywebsite.com/video.m4v
    dimensions
      height 300
      width  400
EOS

The YAML representation looks the following:

---
:caption: foo
:credit: bar
:images:
  :small:
    :url: http://mywebsite.com/image-small.jpg
    :dimensions:
      :height: 500
      :width: 500
  :large:
    :url: http://mywebsite.com/image-large.jpg
    :dimensions:
      :height: 500
      :width: 500
:videos:
  :small:
    :preview: http://mywebsite.com/video.m4v
    :dimensions:
      :height: 300
      :width: 400

As you see, the YAML representation is also using an indentation-sensitive style and in general does look very similar. Mostly there are a couple of additional colons (:) in YAML.

Significant advantages of GDS compared to YAML are schema specifiers and the definition of multiple values and multiple key-value pairs in one line, using the vertical bar symbol (|) .

require 'gdstruct'
require 'yaml'

res = GDstruct.c( <<-EOS ).to_yaml
@schema country(name,capital,area,population,vehicleRegistrationCode,iso3166code,callingCode)
, @schema country                                                                                                              /*
  ----------------------------------------------------------------------------------------------------------------------------
    name                  capital            area (km^2)   population      vehicleRegistrationCode   iso3166code   callingCode
  ---------------------------------------------------------------------------------------------------------------------------- */
  : Deutschland         | Berlin           |    357_385  |    82_521_653 | D                       | DE          | 49
  : USA                 | Washington, D.C. |  9_833_520  |   325_719_178 | USA                     | US          | 1
  : China               | Beijing          |  9_596_961  | 1_403_500_365 | CHN                     | CN          | 86
  : India               | New Dehli        |  3_287_469  | 1_339_180_000 | IND                     | IN          | 91
  : Austria             | Vienna           |     83_878  |     8_822_267 | A                       | AT          | 43
  : Denmark             | Copenhagen       |     42_921  |     5_748_769 | DK                      | DK          | 45
  : Canada              | Ottawa           |  9_984_670  |    36_503_097 | CDN                     | CA          | 1
  : France              | Paris            |    643_801  |    66_991_000 | F                       | FR          | 33
  : Russia              | Moscow           | 17_075_400  |   144_526_636 | RUS                     | RU          | 7
EOS

The equivalent YAML representation looks the following:

---
- :name: Deutschland
  :capital: Berlin
  :area: 357385
  :population: 82521653
  :vehicleRegistrationCode: D
  :iso3166code: DE
  :callingCode: 49
- :name: USA
  :capital: Washington, D.C.
  :area: 9833520
  :population: 325719178
  :vehicleRegistrationCode: USA
  :iso3166code: US
  :callingCode: 1
- :name: China
  :capital: Beijing
  :area: 9596961
  :population: 1403500365
  :vehicleRegistrationCode: CHN
  :iso3166code: CN
  :callingCode: 86
- :name: India
  :capital: New Dehli
  :area: 3287469
  :population: 1339180000
  :vehicleRegistrationCode: IND
  :iso3166code: IN
  :callingCode: 91
- :name: Austria
  :capital: Vienna
  :area: 83878
  :population: 8822267
  :vehicleRegistrationCode: A
  :iso3166code: AT
  :callingCode: 43
- :name: Denmark
  :capital: Copenhagen
  :area: 42921
  :population: 5748769
  :vehicleRegistrationCode: DK
  :iso3166code: DK
  :callingCode: 45
- :name: Canada
  :capital: Ottawa
  :area: 9984670
  :population: 36503097
  :vehicleRegistrationCode: CDN
  :iso3166code: CA
  :callingCode: 1
- :name: France
  :capital: Paris
  :area: 643801
  :population: 66991000
  :vehicleRegistrationCode: F
  :iso3166code: FR
  :callingCode: 33
- :name: Russia
  :capital: Moscow
  :area: 17075400
  :population: 144526636
  :vehicleRegistrationCode: RUS
  :iso3166code: RU
  :callingCode: 7

Here, in my opinion, the GDS representation is clearer.

Practical Examples

There is a collection of practical examples on GitHub:

github.com/uliramminger/gds-examples

The following are a couple examples of Rails applications using the GDS language to seed initial data into the database.

Countries01

rails/countries01

This example includes only one Active Record model: Country

er diagram

The file to seed initial data into the database (db/seeds.rb) looks the following

# db/seeds.rb

countries = GDstruct.c( <<-EOS )
@schema country( name, capital, area, population, vehicleRegistrationCode, iso3166code, callingCode )
, @schema country                                                                                                              /*
  ----------------------------------------------------------------------------------------------------------------------------
    name            capital            area (km^2)   population      vehicleRegistrationCode   iso3166code   callingCode
  ---------------------------------------------------------------------------------------------------------------------------- */
  : Australia     | Canberra         |  7_692_024  |    25_130_600 | AUS                     | AU          | 61
  : Austria       | Vienna           |     83_878  |     8_822_267 | A                       | AT          | 43
  : Belgium       | Brussels         |     30_528  |    11_420_163 | B                       | BE          | 32
  : Brazil        | Brazília         |  8_515_767  |   210_147_125 | BR                      | BR          | 55
  : Canada        | Ottawa           |  9_984_670  |    36_503_097 | CDN                     | CA          | 1
  : China         | Beijing          |  9_596_961  | 1_403_500_365 | CHN                     | CN          | 86
  : Costa Rica    | San José         |     51_100  |     4_857_274 | CR                      | CR          | 506
  : Denmark       | Copenhagen       |     42_921  |     5_748_769 | DK                      | DK          | 45
  : Deutschland   | Berlin           |    357_385  |    82_521_653 | D                       | DE          | 49
  : Estonia       | Tallinn          |     45_227  |     1_319_133 | EST                     | EE          | 372
  : France        | Paris            |    643_801  |    66_991_000 | F                       | FR          | 33
  : India         | New Dehli        |  3_287_469  | 1_339_180_000 | IND                     | IN          | 91
  : Mauritius     | Port Louis       |      2_040  |     1_262_132 | MS                      | MU          | 230
  : Niger         | Niamey           |  1_267_000  |    20_672_987 | RN                      | NE          | 227
  : Nigeria       | Abuja            |    923_768  |   190_886_311 | WAN                     | NG          | 234
  : Russia        | Moscow           | 17_075_400  |   144_526_636 | RUS                     | RU          | 7
  : USA           | Washington, D.C. |  9_833_520  |   325_719_178 | USA                     | US          | 1
EOS

countries.each do |c|
  country = Country.create!( c )
end

Countries02

rails/countries02

This example includes two Active Record models: Country and City.
There is a one-to-many relationship between Country and City. One country has many cities.

er diagram

The file to seed initial data into the database (db/seeds.rb) looks the following

# db/seeds.rb

countries = GDstruct.c( <<-EOS )
@schema country( name, capital, area, population, vehicleRegistrationCode, iso3166code, callingCode )
@schema city( name, population )
, @schema country                                                                                                                                                /*
  -------------------------------------------------------------------------------------------------------------------------------------------------------------- 
    name            capital            area (km^2)   population      vehicleRegistrationCode   iso3166code   callingCode ||  name                 population
  -------------------------------------------------------------------------------------------------------------------------------------------------------------- */
  : Australia     | Canberra         |  7_692_024  |    25_130_600 | AUS                     | AU          | 61
    bigCities , @schema city
      :                                                                                                                      Sydney            |  5_131_326
      :                                                                                                                      Melbourne         |  4_850_740
      :                                                                                                                      Brisbane          |  2_408_223
  : Austria       | Vienna           |     83_878  |     8_822_267 | A                       | AT          | 43
    bigCities , @schema city
      :                                                                                                                      Vienna            |  1_840_573
      :                                                                                                                      Graz              |    272_838
      :                                                                                                                      Linz              |    198_181
      :                                                                                                                      Salzburg          |    148_420
      :                                                                                                                      Innsbruck         |    126_851
  : Brazil        | Brazília         |  8_515_767  |   210_147_125 | BR                      | BR          | 55
  : Canada        | Ottawa           |  9_984_670  |    36_503_097 | CDN                     | CA          | 1
    bigCities , @schema city
      :                                                                                                                      Toronto           |  2_731_571
  : China         | Beijing          |  9_596_961  | 1_403_500_365 | CHN                     | CN          | 86
    bigCities , @schema city
      :                                                                                                                      Chongqing         | 30_165_500
      :                                                                                                                      Shanghai          | 24_183_300
      :                                                                                                                      Beijing           | 21_707_000
      :                                                                                                                      Guangzhou         | 13_081_000
      :                                                                                                                      Tianjin           | 11_249_000
  : France        | Paris            |    643_801  |    66_991_000 | F                       | FR          | 33
    bigCities , @schema city
      :                                                                                                                      Paris             |  2_229_621
      :                                                                                                                      Marseille         |    855_393
  : Mauritius     | Port Louis       |      2_040  |     1_262_132 | MS                      | MU          | 230
  : Niger         | Niamey           |  1_267_000  |    20_672_987 | RN                      | NE          | 227
  : Russia        | Moscow           | 17_075_400  |   144_526_636 | RUS                     | RU          | 7
    bigCities , @schema city
      :                                                                                                                      Moscow            | 12_380_664
      :                                                                                                                      Saint Petersburg  |  5_281_579
      :                                                                                                                      Novosibirsk       |  1_602_915
      :                                                                                                                      Yekaterinburg     |  1_455_514
  : USA           | Washington, D.C. |  9_833_520  |   325_719_178 | USA                     | US          | 1
EOS

countries.each do |countrydef|
  country = Country.create!( countrydef.except(:bigCities) )
  country.bigCities.create!( countrydef[:bigCities] )  if countrydef[:bigCities]
end

Countries03

rails/countries03

This example includes four Active Record models: Country, City, Museum and Person.

There are a couple of one-to-many relationships:

  • between Country and City: one country has many cities
  • between City and Museum: one city has many museums
  • between Country and Person: one country has many famous people

er diagram

The file to seed initial data into the database (db/seeds.rb) looks the following

# db/seeds.rb

countries = GDstruct.c( <<-EOS )
@schema country( name, capital, area, population, vehicleRegistrationCode, iso3166code, callingCode )
@schema city( name, population )
@schema museum( name, established )
@schema person( firstname, lastname, yearOfBirth )
, @schema country                                                                                                                                                                                                                                                         /*
  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 
    Country                                                                                                              ||  City                              ||  Museum                                      ||  Person
    name            capital            area (km^2)   population      vehicleRegistrationCode   iso3166code   callingCode ||  name                 population   ||  name                   | established (year) ||  firstname         | lastname         | year of birth
  ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
  : Austria       | Vienna           |     83_878  |     8_822_267 | A                       | AT          | 43
    bigCities , @schema city
      :                                                                                                                      Vienna            |  1_840_573
      :                                                                                                                      Graz              |    272_838
      :                                                                                                                      Linz              |    198_181
    famousPeople, @schema person
      :                                                                                                                                                                                                            Wolfgang Amadeus  | Mozart           | 1756
      :                                                                                                                                                                                                            Kurt              | Gödel            | 1906
  : Deutschland   | Berlin           |    357_385  |    82_521_653 | D                       | DE          | 49
    bigCities , @schema city
      :                                                                                                                      Berlin            |  3_613_495
        museums, @schema museum
          :                                                                                                                                                        Pergamon Museum        | 1910
          :                                                                                                                                                        Bode Museum            | 1904
          :                                                                                                                                                        Bauhaus Archive        | 1960
          :                                                                                                                                                        Natural History Museum | 1889
      :                                                                                                                      Munich            |  1_456_039
        museums, @schema museum
          :                                                                                                                                                        Deutsches Museum       | 1903
    famousPeople, @schema person
      :                                                                                                                                                                                                            Albert            | Einstein         | 1879
      :                                                                                                                                                                                                            Friedrich         | Schiller         | 1759
      :                                                                                                                                                                                                            Johann Sebastian  | Bach             | 1685
  : France        | Paris            |    643_801  |    66_991_000 | F                       | FR          | 33
    bigCities , @schema city
      :                                                                                                                      Paris             |  2_229_621
      :                                                                                                                      Marseille         |    855_393
  : USA           | Washington, D.C. |  9_833_520  |   325_719_178 | USA                     | US          | 1
EOS

countries.each do |countrydef|
  country = Country.create!( countrydef.except(:bigCities,:famousPeople) )

  countrydef[:bigCities].each do |citydef|
    city = City.new( citydef.except(:museums) )
    country.bigCities << city
    city.museums.create!( citydef[:museums] )  if citydef[:museums]
  end  if countrydef[:bigCities]

  country.famousPeople.create!( countrydef[:famousPeople] )  if countrydef[:famousPeople]
end

Countries04

rails/countries04

This example includes two Active Record models: Currency and Country.
There is a one-to-many relationship between Currency and Country. One currency is used in (has many) countries.

er diagram

This time there are references used to establish the relationship between Country and Currency.

The file to seed initial data into the database (db/seeds.rb) looks the following

# db/seeds.rb

data = GDstruct.c( <<-EOS )

currencies , @schema( name, symbol, iso4217code, iso4217number, subunit )                                      /*
  ------------------------------------------------------------------------------------------------------------
                           name                          symbol     iso4217code   iso4217number  subunit
  ------------------------------------------------------------------------------------------------------------ */
  &australian_dollar     : Australian dollar           | '$'      | AUD         |  36          | cent
  &brazilian_real        : Brazilian real              | 'R$'     | BRL         | 986          | centavo
  &euro                  : Euro                        | '€'      | EUR         | 978          | cent
  &united_states_dollar  : United States dollar        | '$'      | USD         | 840          | cent

countries , @schema( name, capital, area, population, currency )                                              /*
  -----------------------------------------------------------------------------------------------------------
    name                        capital            area (km^2)   population      currency
  ----------------------------------------------------------------------------------------------------------- */
  : Australia                 | Canberra         |  7_692_024  |    25_130_600 | *australian_dollar
  : Austria                   | Vienna           |     83_878  |     8_822_267 | *euro
  : Brazil                    | Brasília         |  8_515_767  |   210_147_125 | *brazilian_real
  : British Virgin Islands    | Road Town        |        153  |        31_758 | *united_states_dollar
  : Christmas Island          | Flying Fish Cove |        135  |         1_843 | *australian_dollar
  : Deutschland               | Berlin           |    357_385  |    82_521_653 | *euro
  : France                    | Paris            |    643_801  |    66_991_000 | *euro
  : Guam                      | Hagåtña          |        540  |       162_742 | *united_states_dollar
  : Kiribati                  | Tarawa           |        811  |       110_136 | *australian_dollar
  : Marshall Islands          | Majuro           |        181  |        53_066 | *united_states_dollar
  : Nauru                     | Yaren            |         21  |        11_200 | *australian_dollar
  : Palau                     | Melekeok         |        459  |        21_503 | *united_states_dollar
  : Puerto Rico               | San Juan         |      9_104  |     3_337_177 | *united_states_dollar
  : USA                       | Washington, D.C. |  9_833_520  |   325_719_178 | *united_states_dollar

EOS

data[:currencies].each do |currencydef|
  currency = Currency.create!( currencydef )
  currencydef[:id] = currency.id
end

data[:countries].each do |countrydef|
  Country.create!( countrydef.merge( currency_id: countrydef[:currency][:id] ).except(:currency) )
end

Books01

rails/books01

This example includes three Active Record models: Book, Author and Publication.
There is a many-to-many relationship between Book and Author.
One book can be written by many (has many) authors and one author can write (has many) books.
The Publication model serves as a join model to setup the many-to-many relationship.

er diagram

There are references used to establish the relationship between Book and Author.

The file to seed initial data into the database (db/seeds.rb) looks the following

# db/seeds.rb

data = GDstruct.c( <<-EOS )

authors , @schema( firstname, lastname )                     /*
  ----------------------------------------------------------
                  first name     last name
  ---------------------------------------------------------- */
  &h_abelson    : Harold       | Abelson
  &e_gamma      : Erich        | Gamma
  &j_goerzen    : John         | Goerzen
  &r_helm       : Richard      | Helm
  &r_johnson    : Ralph        | Johnson
  &dr_musser    : David R.     | Musser
  &b_o_sullivan : Bryan        | O'Sullivan
  &r_olsen      : Russ         | Olsen
  &a_saini      : Atul         | Saini
  &d_stewart    : Don          | Stewart
  &gj_sussman   : Gerald Jay   | Sussman
  &j_sussman    : Julie        | Sussman
  &j_vlissides  : John         | Vlissides

books , @schema book( title, subtitle, year, isbn )                                                                                           /*
  -------------------------------------------------------------------------------------------------------------------------------------------
    title                                                subtitle                                                  year   ISBN number
  ------------------------------------------------------------------------------------------------------------------------------------------- */
  : Design Patterns                                    | Elements of Reusable Object-Oriented Software           | 1995 | '0-201-63361-2'
    authors, *e_gamma | *r_helm | *r_johnson | *j_vlissides
  : Design Patterns in Ruby                            | @na                                                     | 2008 | '0-321-49045-2'
    authors, *r_olsen
  : Real World Haskell                                 | @na                                                     | 2009 | '0-596-51498-3'
    authors, *b_o_sullivan | *j_goerzen | *d_stewart
  : STL Tutorial and Reference Guide                   | C++ Programming with the Standard Template Library      | 1996 | '0-201-63398-1'
    authors, *dr_musser | *a_saini
  : Structure and Interpretation of Computer Programs  | Second Edition                                          | 1996 | '0-262-51087-1'
    authors, *h_abelson | *gj_sussman | *j_sussman

EOS

data[:authors].each do |authordef|
  author = Author.create!( authordef )
  authordef[:id] = author.id
end

data[:books].each do |bookdef|
  book = Book.create!( bookdef.except(:authors) )

  bookdef[:authors].each do |authordef|
    book.publications.create!( book_id: book.id, author_id: authordef[:id] )
  end
end

Implementation

For the parsing of GDS expressions the treetop gem is used.

For the creation of the treetop’s grammar definition for the GDS language, a tool called LDL (Language Definition Language) generator is used.
The LDL metalanguage is a special language for the definition of other languages and provides a higher abstraction for language definitions and the creation of abstract syntax trees.

The LDL generator is homemade and is not published yet.

Ruby Gem

You can find it on RubyGems.org:

rubygems.org/gems/gdstruct

Source Code

You can find the source code on GitHub:

github.com/uliramminger/gdstruct