Skip to content

Instantly share code, notes, and snippets.

@jirutka
Forked from mislav/example.rb
Last active August 31, 2021 16:40
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jirutka/31b1a61162e41d5064fc to your computer and use it in GitHub Desktop.
Save jirutka/31b1a61162e41d5064fc to your computer and use it in GitHub Desktop.
A Psych extension to enable choosing output styles for specific objects.
# default boring output of Psych.dump
---
:response:
:body: ! "{\n \"page\": 1,\n \"results\": [\n \"item\", \"another\"\n ],\n
\ \"total_pages\": 0\n}\n"
:status: 200
:person:
name: Steve
age: 24
:array:
- apples
- bananas
- oranges
---
:response:
# formatted string presented in literal style
:body: |
{
"page": 1,
"results": [
"item", "another"
],
"total_pages": 0
}
:status: 200
# inline hash
:person: {name: Steve, age: 24}
# inline array
:array: [apples, bananas, oranges]
# The MIT License
#
# Copyright 2012 Mislav Marohnić <mislav.marohnic@gmail.com>.
# Copyright 2014 Jakub Jirutka <jakub@jirutka.cz>.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'psych'
require 'stringio'
# A Psych extension to enable choosing output styles for specific objects.
#
# Thanks to Tenderlove for help in <http://stackoverflow.com/q/9640277/11687>.
#
# Example:
#
# data = {
# response: { body: StyledYAML.literal(json_string), status: 200 },
# person: StyledYAML.inline({ 'name' => 'Stevie', 'age' => 12 }),
# array: StyledYAML.inline(%w[ apples bananas oranges ])
# }
#
# StyledYAML.dump(data, $stdout)
#
module StyledYAML
# http://www.yaml.org/spec/1.2/spec.html#id2795688
module LiteralScalar
def yaml_style
Psych::Nodes::Scalar::LITERAL
end
end
# http://www.yaml.org/spec/1.2/spec.html#id2796251
module FoldedScalar
def yaml_style
Psych::Nodes::Scalar::FOLDED
end
end
# http://www.yaml.org/spec/1.2/spec.html#id2790832
module FlowMapping
def yaml_style
Psych::Nodes::Mapping::FLOW
end
end
# http://www.yaml.org/spec/1.2/spec.html#id2790320
module FlowSequence
def yaml_style
Psych::Nodes::Sequence::FLOW
end
end
# Custom tree builder class to recognize scalars tagged with `yaml_style`
class TreeBuilder < Psych::TreeBuilder
attr_writer :next_seq_or_map_style
def next_seq_or_map_style(default_style)
style = @next_seq_or_map_style || default_style
@next_seq_or_map_style = nil
style
end
def scalar(value, anchor, tag, plain, quoted, style)
if style_any?(style) && value.respond_to?(:yaml_style)
if style_literal_or_folded? value.yaml_style
plain = false
quoted = true
end
style = value.yaml_style
end
super
end
def style_any?(style)
Psych::Nodes::Scalar::ANY == style
end
def style_literal_or_folded?(style)
[Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED].include?(style)
end
[:sequence, :mapping].each do |type|
class_eval <<-RUBY
def start_#{type}(anchor, tag, implicit, style)
style = next_seq_or_map_style(style)
super
end
RUBY
end
end
# Custom tree class to handle Hashes and Arrays tagged with `yaml_style`.
class YAMLTree < Psych::Visitors::YAMLTree
[:Hash, :Array, :Psych_Set, :Psych_Omap].each do |klass|
class_eval <<-RUBY
def visit_#{klass}(o)
if o.respond_to? :yaml_style
@emitter.next_seq_or_map_style = o.yaml_style
end
super
end
RUBY
end
end
# Tag string to be output using literal style.
def self.literal(str)
str.extend(LiteralScalar)
str
end
# Tag string to be output using folded style.
def self.folded(str)
str.extend(FoldedScalar)
str
end
# Tag Hashe or Array to be output all on one line.
def self.inline(obj)
case obj
when Hash
obj.extend(FlowMapping)
when Array
obj.extend(FlowSequence)
else
warn "#{self}: unrecognized type to inline (#{obj.class.name})"
end
obj
end
# A Psych.dump alternative that uses the custom TreeBuilder
def self.dump(obj, io = nil, options = {})
real_io = io || StringIO.new(''.encode('utf-8'))
visitor = YAMLTree.create(options, TreeBuilder.new)
visitor << obj
ast = visitor.tree
begin
ast.yaml(real_io)
rescue
# The `yaml` method was introduced in later versions, so fall back to
# constructing a visitor
Psych::Visitors::Emitter.new(real_io).accept(ast)
end
io || real_io.string
end
end
# The MIT License
#
# Copyright 2012 Mislav Marohnić <mislav.marohnic@gmail.com>.
# Copyright 2014 Jakub Jirutka <jakub@jirutka.cz>.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
require 'active_support/core_ext/module/aliasing'
require 'psych'
require 'stringio'
# A Psych extension to enable choosing output styles for specific objects.
# This version monkey patches the Psych classes.
#
# Thanks to Mislav Marohnić for the original implementation in
# <https://gist.github.com/mislav/2023978>.
# Thanks to Tenderlove for help in <http://stackoverflow.com/q/9640277/11687>.
#
# Example:
#
# data = {
# response: { body: StyledYAML.literal(json_string), status: 200 },
# person: StyledYAML.inline({ 'name' => 'Stevie', 'age' => 12 }),
# array: StyledYAML.inline(%w[ apples bananas oranges ])
# }
#
# data.to_yaml
#
module StyledYAML
# http://www.yaml.org/spec/1.2/spec.html#id2795688
module LiteralScalar
def yaml_style
Psych::Nodes::Scalar::LITERAL
end
end
# http://www.yaml.org/spec/1.2/spec.html#id2796251
module FoldedScalar
def yaml_style
Psych::Nodes::Scalar::FOLDED
end
end
# http://www.yaml.org/spec/1.2/spec.html#id2790832
module FlowMapping
def yaml_style
Psych::Nodes::Mapping::FLOW
end
end
# http://www.yaml.org/spec/1.2/spec.html#id2790320
module FlowSequence
def yaml_style
Psych::Nodes::Sequence::FLOW
end
end
# Tag string to be output using literal style.
def self.literal(str)
str.extend(LiteralScalar)
str
end
# Tag string to be output using folded style.
def self.folded(str)
str.extend(FoldedScalar)
str
end
# Tag Hashe or Array to be output all on one line.
def self.inline(obj)
case obj
when Hash
obj.extend(FlowMapping)
when Array
obj.extend(FlowSequence)
else
warn "#{self}: unrecognized type to inline (#{obj.class.name})"
end
obj
end
end
module Psych
# Monkey patch to recognize scalars tagged with `yaml_style`.
class TreeBuilder
attr_writer :next_seq_or_map_style
def next_seq_or_map_style(default_style)
style = @next_seq_or_map_style || default_style
@next_seq_or_map_style = nil
style
end
def scalar_with_style(value, anchor, tag, plain, quoted, style)
if style_any?(style) && value.respond_to?(:yaml_style)
if style_literal_or_folded? value.yaml_style
plain = false
quoted = true
end
style = value.yaml_style
end
scalar_without_style(value, anchor, tag, plain, quoted, style)
end
alias_method_chain :scalar, :style
def style_any?(style)
Psych::Nodes::Scalar::ANY == style
end
def style_literal_or_folded?(style)
[Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED].include?(style)
end
[:sequence, :mapping].each do |type|
class_eval <<-RUBY
def start_#{type}_with_style(anchor, tag, implicit, style)
style = next_seq_or_map_style(style)
start_#{type}_without_style(anchor, tag, implicit, style)
end
alias_method_chain :start_#{type}, :style
RUBY
end
end
# Monkey patch to handle Hashes and Arrays tagged with `yaml_style`.
class Visitors::YAMLTree
[:Hash, :Array, :Psych_Set, :Psych_Omap].each do |klass|
class_eval <<-RUBY
def visit_#{klass}_with_style(o)
if o.respond_to? :yaml_style
@emitter.next_seq_or_map_style = o.yaml_style
end
visit_#{klass}_without_style(o)
end
alias_method_chain :visit_#{klass}, :style
RUBY
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment