Module: Psychgus

Extended by:
PsychDropIn
Defined in:
lib/psychgus.rb,
lib/psychgus/ext.rb,
lib/psychgus/styler.rb,
lib/psychgus/version.rb,
lib/psychgus/blueberry.rb,
lib/psychgus/ext/core_ext.rb,
lib/psychgus/ext/node_ext.rb,
lib/psychgus/super_sniffer.rb,
lib/psychgus/ext/yaml_tree_ext.rb,
lib/psychgus/styled_tree_builder.rb,
lib/psychgus/super_sniffer/parent.rb,
lib/psychgus/styled_document_stream.rb

Overview

Psychgus uses the core standard library Psych for working with YAML and extends it so that developers can easily style the YAML according to their needs. Thank you to the people that worked and continue to work hard on that project.

The name comes from the well-styled character Gus from the TV show Psych.

Create a Styler

First, we will create a Styler.

All you need to do is add include Psychgus::Styler to a class.

Here is a complex Styler for the examples below:

require 'psychgus'

class BurgerStyler
  # Mix in methods needed for styling
  include Psychgus::Styler

  def initialize(sniffer=nil)
    if sniffer.nil?()
      @class_level = 0
      @class_position = 0
    else
      # For the Class Example
      @class_level = sniffer.level
      @class_position = sniffer.position
    end
  end

  # Style all nodes (Psych::Nodes::Node)
  def style(sniffer,node)
    # Remove "!ruby/object:..." for classes
    node.tag = nil if node.node_of?(:mapping,:scalar,:sequence)

    # This is another way to do the above
    #node.tag = nil if node.respond_to?(:tag=)
  end

  # Style aliases (Psych::Nodes::Alias)
  def style_alias(sniffer,node)
  end

  # Style maps (Psych::Nodes::Mapping)
  # - Hashes (key/value pairs)
  # - Example: "Burgers: Classic {}"
  def style_mapping(sniffer,node)
    parent = sniffer.parent

    if !parent.nil?()
      # BBQ
      node.style = Psychgus::MAPPING_FLOW if parent.node_of?(:scalar) &&
                                             parent.value.casecmp('BBQ') == 0
    end
  end

  # Style scalars (Psych::Nodes::Scalar)
  # - Any text (non-alias)
  def style_scalar(sniffer,node)
    parent = sniffer.parent

    # Single quote scalars that are not keys to a map
    node.style = Psychgus::SCALAR_SINGLE_QUOTED if !parent.nil?() && 
                                                   parent.child_type != :key

    # Remove colon (change symbols into strings)
    node.value = node.value.sub(':','')

    # Change lettuce to spinach
    node.value = 'Spinach' if node.value.casecmp('Lettuce') == 0

    # Capitalize each word
    node.value = node.value.split(' ').map do |v|
      if v.casecmp('BBQ') == 0
        v.upcase()
      else
        v.capitalize()
      end
    end.join(' ')
  end

  # Style sequences (Psych::Nodes::Sequence)
  # - Arrays
  # - Example: "[Lettuce, Onions, Pickles, Tomatoes]"
  def style_sequence(sniffer,node)
    relative_level = (sniffer.level - @class_level) + 1

    node.style = Psychgus::SEQUENCE_FLOW if sniffer.level >= 4

    # Make "[Ketchup, Mustard]" a block for the Class Example
    node.style = Psychgus::SEQUENCE_BLOCK if relative_level == 7
  end
end

Examples:

Hash example

require 'psychgus'

burgers = {
  :Burgers => {
    :Classic => {
      :Sauce  => %w(Ketchup Mustard),
      :Cheese => 'American',
      :Bun    => 'Sesame Seed'
    },
    :BBQ => {
      :Sauce  => 'Honey BBQ',
      :Cheese => 'Cheddar',
      :Bun    => 'Kaiser'
    },
    :Fancy => {
      :Sauce  => 'Spicy Wasabi',
      :Cheese => 'Smoked Gouda',
      :Bun    => 'Hawaiian'
    }
  },
  :Toppings => [
    'Mushrooms',
    %w(Lettuce Onions Pickles Tomatoes),
    [%w(Ketchup Mustard), %w(Salt Pepper)]
  ]
}
burgers[:Favorite] = burgers[:Burgers][:BBQ] # Alias

puts burgers.to_yaml(indent: 3,stylers: BurgerStyler.new,deref_aliases: true)

# Output:
# ---
# Burgers:
#    Classic:
#       Sauce: ['Ketchup', 'Mustard']
#       Cheese: 'American'
#       Bun: 'Sesame Seed'
#    BBQ: {Sauce: 'Honey BBQ', Cheese: 'Cheddar', Bun: 'Kaiser'}
#    Fancy:
#       Sauce: 'Spicy Wasabi'
#       Cheese: 'Smoked Gouda'
#       Bun: 'Hawaiian'
# Toppings:
# - 'Mushrooms'
# - ['Spinach', 'Onions', 'Pickles', 'Tomatoes']
# - [['Ketchup', 'Mustard'], ['Salt', 'Pepper']]
# Favorite:
#    Sauce: 'Honey BBQ'
#    Cheese: 'Cheddar'
#    Bun: 'Kaiser'

Class example

require 'psychgus'

class Burger
  attr_accessor :bun
  attr_accessor :cheese
  attr_accessor :sauce

  def initialize(sauce,cheese,bun)
    @bun = bun
    @cheese = cheese
    @sauce = sauce
  end

  # You can still use Psych's encode_with(), no problem
  #def encode_with(coder)
  #  coder['Bun'] = @bun
  #  coder['Cheese'] = @cheese
  #  coder['Sauce'] = @sauce
  #end
end

class Burgers
  include Psychgus::Blueberry

  attr_accessor :burgers
  attr_accessor :toppings
  attr_accessor :favorite

  def initialize()
    @burgers = {
      'Classic' => Burger.new(['Ketchup','Mustard'],'American','Sesame Seed'),
      'BBQ'     => Burger.new('Honey BBQ','Cheddar','Kaiser'),
      'Fancy'   => Burger.new('Spicy Wasabi','Smoked Gouda','Hawaiian')
    }

    @toppings = [
      'Mushrooms',
      %w(Lettuce Onions Pickles Tomatoes),
      [%w(Ketchup Mustard),%w(Salt Pepper)]
    ]

    @favorite = @burgers['BBQ'] # Alias
  end

  def psychgus_stylers(sniffer)
    return BurgerStyler.new(sniffer)
  end

  # You can still use Psych's encode_with(), no problem
  #def encode_with(coder)
  #  coder['Burgers'] = @burgers
  #  coder['Toppings'] = @toppings
  #  coder['Favorite'] = @favorite
  #end
end

burgers = Burgers.new
puts burgers.to_yaml(indent: 3,deref_aliases: true)

# Output:
# ---
# Burgers:
#    Classic:
#       Bun: 'Sesame Seed'
#       Cheese: 'American'
#       Sauce:
#       - 'Ketchup'
#       - 'Mustard'
#    BBQ: {Bun: 'Kaiser', Cheese: 'Cheddar', Sauce: 'Honey BBQ'}
#    Fancy:
#       Bun: 'Hawaiian'
#       Cheese: 'Smoked Gouda'
#       Sauce: 'Spicy Wasabi'
# Toppings:
# - 'Mushrooms'
# - ['Spinach', 'Onions', 'Pickles', 'Tomatoes']
# - [['Ketchup', 'Mustard'], ['Salt', 'Pepper']]
# Favorite:
#    Bun: 'Kaiser'
#    Cheese: 'Cheddar'
#    Sauce: 'Honey BBQ'

Emitting / Parsing examples

styler = BurgerStyler.new()
options = {:indentation=>3,:stylers=>styler,:deref_aliases=>true}
yaml = burgers.to_yaml(options)

# High-level emitting
Psychgus.dump(burgers,options)
Psychgus.dump_file('burgers.yaml',burgers,options)
burgers.to_yaml(options)

# High-level parsing
# - Because to_ruby() will be called, just use Psych:
#   - load(), load_file(), load_stream(), safe_load()

# Mid-level emitting
stream = Psychgus.parse_stream(yaml,stylers: styler,deref_aliases: true)

stream.to_yaml()

# Mid-level parsing
Psychgus.parse(yaml,stylers: styler,deref_aliases: true)
Psychgus.parse_file('burgers.yaml',stylers: styler,deref_aliases: true)
Psychgus.parse_stream(yaml,stylers: styler,deref_aliases: true)

# Low-level emitting
tree_builder = Psychgus::StyledTreeBuilder.new(styler,deref_aliases: true)
visitor = Psych::Visitors::YAMLTree.create(options,tree_builder)

visitor << burgers
visitor.tree.to_yaml

# Low-level parsing
parser = Psychgus.parser(stylers: styler,deref_aliases: true)

parser.parse(yaml)
parser.handler
parser.handler.root

Author:

  • Jonathan Bradley Whited (@esotericpig)

Since:

  • 1.0.0

Defined Under Namespace

Modules: Blueberry, Ext, PsychDropIn, Styler Classes: StyledDocumentStream, StyledTreeBuilder, SuperSniffer

Constant Summary collapse

NODE_CLASS_ALIASES =

Since:

  • 1.0.0

{:Doc => :Document,:Map => :Mapping,:Seq => :Sequence}
OPTIONS_ALIASES =

Since:

  • 1.0.0

{:canon => :canonical,:indent => :indentation}
MAPPING_ANY =

Since:

  • 1.0.0

node_const(:mapping,:any)
MAPPING_BLOCK =

Since:

  • 1.0.0

node_const(:mapping,:block)
MAPPING_FLOW =

Since:

  • 1.0.0

node_const(:mapping,:flow)
MAP_ANY =

Since:

  • 1.0.0

MAPPING_ANY
MAP_BLOCK =

Since:

  • 1.0.0

MAPPING_BLOCK
MAP_FLOW =

Since:

  • 1.0.0

MAPPING_FLOW
SCALAR_ANY =

Since:

  • 1.0.0

node_const(:scalar,:any)
SCALAR_PLAIN =

Since:

  • 1.0.0

node_const(:scalar,:plain)
SCALAR_SINGLE_QUOTED =

Since:

  • 1.0.0

node_const(:scalar,:single_quoted)
SCALAR_DOUBLE_QUOTED =

Since:

  • 1.0.0

node_const(:scalar,:double_quoted)
SCALAR_LITERAL =

Since:

  • 1.0.0

node_const(:scalar,:literal)
SCALAR_FOLDED =

Since:

  • 1.0.0

node_const(:scalar,:folded)
SEQUENCE_ANY =

Since:

  • 1.0.0

node_const(:sequence,:any)
SEQUENCE_BLOCK =

Since:

  • 1.0.0

node_const(:sequence,:block)
SEQUENCE_FLOW =

Since:

  • 1.0.0

node_const(:sequence,:flow)
SEQ_ANY =

Since:

  • 1.0.0

SEQUENCE_ANY
SEQ_BLOCK =

Since:

  • 1.0.0

SEQUENCE_BLOCK
SEQ_FLOW =

Since:

  • 1.0.0

SEQUENCE_FLOW
STREAM_ANY =

Since:

  • 1.0.0

node_const(:stream,:any)
STREAM_UTF8 =

Since:

  • 1.0.0

node_const(:stream,:utf8)
STREAM_UTF16LE =

Since:

  • 1.0.0

node_const(:stream,:utf16le)
STREAM_UTF16BE =

Since:

  • 1.0.0

node_const(:stream,:utf16be)
VERSION =

Version of this gem in “#.#.#” format

Since:

  • 1.0.0

'1.0.1'

Class Method Summary collapse

Methods included from PsychDropIn

add_builtin_type, add_domain_type, add_tag, load, load_file, load_stream, remove_type, safe_load, to_json

Class Method Details

.dump(object, io = nil, **options) ⇒ String, Object

Convert object to YAML and dump to io.

object, io, and options are used like in Psych.dump so can be a drop-in replacement for Psych.

Parameters:

  • object (Object)

    the Object to convert to YAML and dump

  • io (nil, IO, Hash) (defaults to: nil)

    the IO to dump the YAML to or the options Hash; if nil, will use StringIO

  • options (Hash)

    the options (or keyword args) to use; see dump_stream

Returns:

  • (String, Object)

    the result of converting object to YAML using the params

See Also:

Since:

  • 1.0.0


391
392
393
# File 'lib/psychgus.rb', line 391

def self.dump(object,io=nil,**options)
  return dump_stream(object,io: io,**options)
end

.dump_file(filename, *objects, mode: 'w', perm: nil, opt: nil, **options) ⇒ Object

Convert objects to YAML and dump to a file.

Examples:

Psychgus.dump_file('my_dir/my_file.yaml',my_object1,my_object2,mode: 'w:UTF-16',
                   stylers: MyStyler.new())
Psychgus.dump_file('my_file.yaml',my_object,stylers: [MyStyler1.new(),MyStyler2.new()])

Parameters:

  • filename (String)

    the name of the file (and path) to dump to

  • objects (Object, Array<Object>)

    the Object(s) to convert to YAML and dump

  • mode (String, Integer) (defaults to: 'w')

    the IO open mode to use; examples:

    'w:UTF-8'

    create a new file or truncate an existing file and use UTF-8 encoding;

    'a:UTF-16'

    create a new file or append to an existing file and use UTF-16 encoding

  • perm (Integer) (defaults to: nil)

    the permission bits to use (platform dependent)

  • opt (Symbol) (defaults to: nil)

    the option(s) to use, more readable alternative to mode; examples: :textmode, :autoclose

  • options (Hash)

    the options (or keyword args) to use; see dump_stream

See Also:

Since:

  • 1.0.0


418
419
420
421
422
# File 'lib/psychgus.rb', line 418

def self.dump_file(filename,*objects,mode: 'w',perm: nil,opt: nil,**options)
  File.open(filename,mode,perm,opt) do |file|
    file.write(dump_stream(*objects,**options))
  end
end

.dump_stream(*objects, io: nil, stylers: nil, deref_aliases: false, **options) ⇒ String, Object

Convert objects to YAML and dump to io.

io and options are used like in Psych.dump so can be a drop-in replacement for Psych.

Parameters:

  • objects (Object, Array<Object>)

    the Object(s) to convert to YAML and dump

  • io (nil, IO, Hash) (defaults to: nil)

    the IO to dump the YAML to or the options Hash; if nil, will use StringIO

  • stylers (nil, Styler, Array<Styler>) (defaults to: nil)

    the Styler(s) to use when converting to YAML

  • deref_aliases (true, false) (defaults to: false)

    whether to dereference aliases; output the actual value instead of the alias

  • options (Hash)

    the options (or keyword args) to use when converting to YAML:

    :indent

    Alias for :indentation. :indentation will override this.

    :indentation

    Default: 2. Number of space characters used to indent. Acceptable value should be in 0..9 range, else ignored.

    :line_width

    Default: 0 (meaning “wrap at 81”). Max character to wrap line at.

    :canon

    Alias for :canonical. :canonical will override this.

    :canonical

    Default: false. Write “canonical” YAML form (very verbose, yet strictly formal).

    :header

    Default: false. Write %YAML [version] at the beginning of document.

Returns:

  • (String, Object)

    the result of converting object to YAML using the params

See Also:

Since:

  • 1.0.0


450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/psychgus.rb', line 450

def self.dump_stream(*objects,io: nil,stylers: nil,deref_aliases: false,**options)
  if Hash === io
    options = io
    io = nil
  end
  
  if !options.nil?()
    OPTIONS_ALIASES.each do |option_alias,option|
      if options.key?(option_alias) && !options.key?(option)
        options[option] = options[option_alias]
      end
    end
  end
  
  visitor = Psych::Visitors::YAMLTree.create(options,StyledTreeBuilder.new(*stylers,
    deref_aliases: deref_aliases))
  
  objects.each do |object|
    visitor << object
  end
  
  return visitor.tree.yaml(io,options)
end

.node_class(name) ⇒ Class

Get a Class (constant) from Psych::Nodes.

Some names have aliases:

:doc => :document
:map => :mapping
:seq => :sequence

Parameters:

  • name (Symbol, String)

    the name of the class from Psych::Nodes

Returns:

  • (Class)

    a class from Psych::Nodes

See Also:

Since:

  • 1.0.0


327
328
329
330
331
332
333
334
# File 'lib/psychgus.rb', line 327

def self.node_class(name)
  name = name.to_sym().capitalize()
  
  name_alias = NODE_CLASS_ALIASES[name]
  name = name_alias unless name_alias.nil?()
  
  return Psych::Nodes.const_get(name)
end

.node_const(class_name, const_name, lenient = true) ⇒ Integer, Object

Get a constant from a Psych::Nodes class (using node_class).

Parameters:

  • class_name (Symbol, String)

    the name of the class to get using node_class

  • const_name (Symbol, String)

    the constant to get from the class

  • lenient (true, false) (defaults to: true)

    if true, will return 0 if not const_defined?(), else raise an error

Returns:

  • (Integer, Object)

    the constant value from the class (usually an int)

See Also:

Since:

  • 1.0.0


345
346
347
348
349
350
351
# File 'lib/psychgus.rb', line 345

def self.node_const(class_name,const_name,lenient=true)
  node_class = node_class(class_name)
  const_name = const_name.to_sym().upcase()
  
  return 0 if lenient && !node_class.const_defined?(const_name,true)
  return node_class.const_get(const_name,true)
end

.parse(yaml, **kargs) ⇒ Psych::Nodes::Document

Parse yaml into a Psych::Nodes::Document.

If you're just going to call to_ruby(), then using this method is unnecessary, and the styler(s) will do nothing for you.

Parameters:

  • yaml (String)

    the YAML to parse

  • kargs (Hash)

    the keyword args to use; see parse_stream

Returns:

  • (Psych::Nodes::Document)

    the parsed Document node

See Also:

Since:

  • 1.0.0


487
488
489
490
491
492
493
# File 'lib/psychgus.rb', line 487

def self.parse(yaml,**kargs)
  parse_stream(yaml,**kargs) do |node|
    return node
  end
  
  return false
end

.parse_file(filename, fallback: false, mode: 'r:BOM|UTF-8', **kargs) ⇒ Psych::Nodes::Document

Parse a YAML file into a Psych::Nodes::Document.

If you're just going to call to_ruby(), then using this method is unnecessary, and the styler(s) will do nothing for you.

Parameters:

  • filename (String)

    the name of the YAML file (and path) to parse

  • fallback (Object) (defaults to: false)

    the return value when nothing is parsed

  • mode (String, Integer) (defaults to: 'r:BOM|UTF-8')

    the IO open mode to use; example: 'r:BOM|UTF-8'

  • kargs (Hash)

    the keyword args to use; see parse_stream

Returns:

  • (Psych::Nodes::Document)

    the parsed Document node

See Also:

  • parse_stream
  • Psych.parse_file
  • Psych::Nodes::Document
  • File.open
  • IO.new

Since:

  • 1.0.0


512
513
514
515
516
517
518
# File 'lib/psychgus.rb', line 512

def self.parse_file(filename,fallback: false,mode: 'r:BOM|UTF-8',**kargs)
  result = File.open(filename,mode) do |file|
    parse(file,filename: filename,**kargs)
  end
  
  return result || fallback
end

.parse_stream(yaml, filename: nil, stylers: nil, deref_aliases: false, **options, &block) ⇒ Psych::Nodes::Stream

Parse yaml into a Psych::Nodes::Stream for one document or for multiple documents in one YAML.

If you're just going to call to_ruby(), then using this method is unnecessary, and the styler(s) will do nothing for you.

Examples:

burgers = <<EOY
---
Burgers:
  Classic:
    BBQ: {Sauce: Honey BBQ, Cheese: Cheddar, Bun: Kaiser}
---
Toppings:
- [Mushrooms, Mustard]
- [Salt, Pepper, Pickles]
---
`Invalid`
EOY

i = 0

begin
  Psychgus.parse_stream(burgers,filename: 'burgers.yaml') do |document|
    puts "Document ##{i += 1}"
    puts document.to_ruby
  end
rescue Psych::SyntaxError => err
  puts "File: #{err.file}"
end

# Output:
#   Document #1
#   {"Burgers"=>{"Classic"=>{"BBQ"=>{"Sauce"=>"Honey BBQ", "Cheese"=>"Cheddar", "Bun"=>"Kaiser"}}}}
#   Document #2
#   {"Toppings"=>[["Mushrooms", "Mustard"], ["Salt", "Pepper", "Pickles"]]}
#   File: burgers.yaml

Parameters:

  • yaml (String)

    the YAML to parse

  • filename (String) (defaults to: nil)

    the filename to pass as file to the Error potentially raised

  • stylers (nil, Styler, Array<Styler>) (defaults to: nil)

    the Styler(s) to use when parsing the YAML

  • deref_aliases (true, false) (defaults to: false)

    whether to dereference aliases; output the actual value instead of the alias

  • block (Proc)

    an optional block for parsing multiple documents

Returns:

  • (Psych::Nodes::Stream)

    the parsed Stream node

See Also:

Since:

  • 1.0.0


570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/psychgus.rb', line 570

def self.parse_stream(yaml,filename: nil,stylers: nil,deref_aliases: false,**options,&block)
  if block_given?()
    parser = Psych::Parser.new(StyledDocumentStream.new(*stylers,deref_aliases: deref_aliases,**options,
      &block))
    
    return parser.parse(yaml,filename)
  else
    parser = self.parser(stylers: stylers,deref_aliases: deref_aliases,**options)
    parser.parse(yaml,filename)
    
    return parser.handler.root
  end
end

.parser(stylers: nil, deref_aliases: false, **options) ⇒ Psych::Parser

Create a new styled Psych::Parser for parsing YAML.

Examples:

class CoffeeStyler
  include Psychgus::Styler

  def style_sequence(sniffer,node)
    node.style = Psychgus::SEQUENCE_FLOW
  end
end

coffee = <<EOY
Coffee:
  Roast:
    - Light
    - Medium
    - Dark
  Style:
    - Cappuccino
    - Latte
    - Mocha
EOY

parser = Psychgus.parser(stylers: CoffeeStyler.new)
parser.parse(coffee)
puts parser.handler.root.to_yaml

# Output:
#   Coffee:
#     Roast: [Light, Medium, Dark]
#     Style: [Cappuccino, Latte, Mocha]

Parameters:

  • stylers (nil, Styler, Array<Styler>) (defaults to: nil)

    the Styler(s) to use when parsing the YAML

  • deref_aliases (true, false) (defaults to: false)

    whether to dereference aliases; output the actual value instead of the alias

Returns:

  • (Psych::Parser)

    the new styled Parser

See Also:

Since:

  • 1.0.0


624
625
626
# File 'lib/psychgus.rb', line 624

def self.parser(stylers: nil,deref_aliases: false,**options)
  return Psych::Parser.new(StyledTreeBuilder.new(*stylers,deref_aliases: deref_aliases,**options))
end