Browse Source

Refactoring out LayoutParser

dev
Andy Meneely 11 years ago
parent
commit
31e5d75f59
  1. 66
      lib/squib/deck.rb
  2. 75
      lib/squib/layout_parser.rb
  3. 184
      spec/deck_spec.rb
  4. 176
      spec/layout_parser_spec.rb

66
lib/squib/deck.rb

@ -5,6 +5,7 @@ require 'squib/card'
require 'squib/progress' require 'squib/progress'
require 'squib/input_helpers' require 'squib/input_helpers'
require 'squib/constants' require 'squib/constants'
require 'squib/layout_parser'
# The project module # The project module
# #
@ -65,7 +66,7 @@ module Squib
cards.times{ @cards << Squib::Card.new(self, width, height) } cards.times{ @cards << Squib::Card.new(self, width, height) }
show_info(config, layout) show_info(config, layout)
load_config(config) load_config(config)
load_layout(layout) @layout = LayoutParser.load_layout(layout)
if block_given? if block_given?
instance_eval(&block) instance_eval(&block)
end end
@ -106,69 +107,6 @@ module Squib
end end
end end
# Load the layout configuration file, if exists
# @api private
def load_layout(files)
@layout = {}
Squib::logger.info { " using layout(s): #{files}" }
Array(files).each do |file|
thefile = file
thefile = "#{File.dirname(__FILE__)}/layouts/#{file}" unless File.exists?(file)
if File.exists? thefile
yml = @layout.merge(YAML.load_file(thefile) || {}) #load_file returns false on empty file
yml.each do |key, value|
@layout[key] = recurse_extends(yml, key, {})
end
else
puts "the file: #{thefile}"
Squib::logger.error { "Layout file not found: #{file}" }
end
end
end
# Process the extends recursively
# :nodoc:
# @api private
def recurse_extends(yml, key, visited )
assert_not_visited(key, visited)
return yml[key] unless has_extends?(yml, key)
visited[key] = key
parent_keys = [yml[key]['extends']].flatten
h = {}
parent_keys.each do |parent_key|
from_extends = yml[key].merge(recurse_extends(yml, parent_key, visited)) do |key, child_val, parent_val|
if child_val.to_s.strip.start_with?('+=')
parent_val + child_val.sub('+=','').strip.to_f
elsif child_val.to_s.strip.start_with?('-=')
parent_val - child_val.sub('-=','').strip.to_f
else
child_val #child overrides parent when merging, no +=
end
end
h = h.merge(from_extends) do |key, older_sibling, younger_sibling|
younger_sibling #when two siblings have the same entry, the "younger" (lower one) overrides
end
end
return h
end
# Does this layout entry have an extends field?
# i.e. is it a base-case or will it need recursion?
# :nodoc:
# @api private
def has_extends?(yml, key)
!!yml[key] && yml[key].key?('extends')
end
# Safeguard against malformed circular extends
# :nodoc:
# @api private
def assert_not_visited(key, visited)
if visited.key? key
raise "Invalid layout: circular extends with '#{key}'"
end
end
# Use Logger to show more detail on the run # Use Logger to show more detail on the run
# :nodoc: # :nodoc:
# @api private # @api private

75
lib/squib/layout_parser.rb

@ -0,0 +1,75 @@
module Squib
# Internal class for handling layouts
#@api private
class LayoutParser
# Load the layout file(s), if exists
# @api private
def self.load_layout(files)
layout = {}
Squib::logger.info { " using layout(s): #{files}" }
Array(files).each do |file|
thefile = file
thefile = builtin(file) unless File.exists?(file)
if File.exists? thefile
yml = layout.merge(YAML.load_file(thefile) || {}) #load_file returns false on empty file
yml.each do |key, value|
layout[key] = recurse_extends(yml, key, {})
end
else
Squib::logger.error { "Layout file not found: #{file}. Skipping..." }
end
end
return layout
end
# Determine the file path of the built-in layout file
def self.builtin(file)
"#{File.dirname(__FILE__)}/layouts/#{file}"
end
# Process the extends recursively
# :nodoc:
# @api private
def self.recurse_extends(yml, key, visited )
assert_not_visited(key, visited)
return yml[key] unless has_extends?(yml, key)
visited[key] = key
parent_keys = [yml[key]['extends']].flatten
h = {}
parent_keys.each do |parent_key|
from_extends = yml[key].merge(recurse_extends(yml, parent_key, visited)) do |key, child_val, parent_val|
if child_val.to_s.strip.start_with?('+=')
parent_val + child_val.sub('+=','').strip.to_f
elsif child_val.to_s.strip.start_with?('-=')
parent_val - child_val.sub('-=','').strip.to_f
else
child_val #child overrides parent when merging, no +=
end
end
h = h.merge(from_extends) do |key, older_sibling, younger_sibling|
younger_sibling #when two siblings have the same entry, the "younger" (lower one) overrides
end
end
return h
end
# Does this layout entry have an extends field?
# i.e. is it a base-case or will it need recursion?
# :nodoc:
# @api private
def self.has_extends?(yml, key)
!!yml[key] && yml[key].key?('extends')
end
# Safeguard against malformed circular extends
# :nodoc:
# @api private
def self.assert_not_visited(key, visited)
if visited.key? key
raise "Invalid layout: circular extends with '#{key}'"
end
end
end
end

184
spec/deck_spec.rb

@ -45,180 +45,16 @@ describe Squib::Deck do
end end
end end
context '#load_layout' do it 'loads a normal layout with no extends' do
d = Squib::Deck.new(layout: layout_file('no-extends.yml'))
it 'loads a normal layout with no extends' do expect(d.layout).to eq({
d = Squib::Deck.new(layout: layout_file('no-extends.yml')) 'frame' => {
expect(d.layout).to \ 'x' => 38,
eq({'frame' => { 'valign' => :middle,
'x' => 38, 'str' => 'blah',
'valign' => :middle, 'font' => 'Mr. Font',
'str' => 'blah', }
'font' => 'Mr. Font', })
}
}
)
end
it 'loads with a single extends' do
d = Squib::Deck.new(layout: layout_file('single-extends.yml'))
expect(d.layout).to \
eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
}
}
)
end
it 'applies the extends regardless of order' do
d = Squib::Deck.new(layout: layout_file('pre-extends.yml'))
expect(d.layout).to \
eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
}
}
)
end
it 'applies the single-level extends multiple times' do
d = Squib::Deck.new(layout: layout_file('single-level-multi-extends.yml'))
expect(d.layout).to \
eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
},
'title2' => {
'extends' => 'frame',
'x' => 75,
'y' => 150,
'width' => 150,
},
}
)
end
it 'applies multiple extends in a single rule' do
d = Squib::Deck.new(layout: layout_file('multi-extends-single-entry.yml'))
expect(d.layout).to \
eq({'aunt' => {
'a' => 101,
'b' => 102,
'c' => 103,
},
'uncle' => {
'x' => 104,
'y' => 105,
'b' => 106,
},
'child' => {
'extends' => ['uncle','aunt'],
'a' => 107, # my own
'b' => 102, # from the younger aunt
'c' => 103, # from aunt
'x' => 108, # my own
'y' => 105, # from uncle
},
}
)
end
it 'applies multi-level extends' do
d = Squib::Deck.new(layout: layout_file('multi-level-extends.yml'))
expect(d.layout).to \
eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
},
'subtitle' => {
'extends' => 'title',
'x' => 38,
'y' => 150,
'width' => 100,
},
}
)
end
it 'fails on a self-circular extends' do
file = layout_file('self-circular-extends.yml')
expect { Squib::Deck.new(layout: file) }.to \
raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
end
it 'fails on a easy-circular extends' do
file = layout_file('easy-circular-extends.yml')
expect { Squib::Deck.new(layout: file) }.to \
raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
end
it 'hard on a easy-circular extends' do
file = layout_file('hard-circular-extends.yml')
expect { Squib::Deck.new(layout: file) }.to \
raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
end
it 'redefines keys on multiple layouts' do
a = layout_file('multifile-a.yml')
b = layout_file('multifile-b.yml')
d = Squib::Deck.new(layout: [a, b])
expect(d.layout).to eq({
'title' => { 'x' => 300 },
'subtitle' => { 'x' => 200 },
'desc' => { 'x' => 400 }
})
end
it 'evaluates extends on each file first' do
a = layout_file('multifile-extends-a.yml')
b = layout_file('multifile-extends-b.yml')
d = Squib::Deck.new(layout: [a, b])
expect(d.layout).to eq({
'grandparent' => { 'x' => 100 },
'parent_a' => { 'x' => 110, 'extends' => 'grandparent' },
'parent_b' => { 'x' => 130, 'extends' => 'grandparent' },
'child_a' => { 'x' => 113, 'extends' => 'parent_a' },
'child_b' => { 'x' => 133, 'extends' => 'parent_b' }
})
end
it 'loads nothing on an empty layout file' do
d = Squib::Deck.new(layout: layout_file('empty.yml'))
expect(d.layout).to eq({})
end
it 'handles extends on a rule with no args' do
d = Squib::Deck.new(layout: layout_file('empty-rule.yml'))
expect(d.layout).to eq({
'empty' => nil
})
end
end end
end end

176
spec/layout_parser_spec.rb

@ -0,0 +1,176 @@
require 'spec_helper'
describe Squib::LayoutParser do
it 'loads a normal layout with no extends' do
layout = Squib::LayoutParser.load_layout(layout_file('no-extends.yml'))
expect(layout).to eq({'frame' => {
'x' => 38,
'valign' => :middle,
'str' => 'blah',
'font' => 'Mr. Font',
}
}
)
end
it 'loads with a single extends' do
layout = Squib::LayoutParser.load_layout(layout_file('single-extends.yml'))
expect(layout).to eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
}
}
)
end
it 'applies the extends regardless of order' do
layout = Squib::LayoutParser.load_layout(layout_file('pre-extends.yml'))
expect(layout).to eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
}
}
)
end
it 'applies the single-level extends multiple times' do
layout = Squib::LayoutParser.load_layout(layout_file('single-level-multi-extends.yml'))
expect(layout).to eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
},
'title2' => {
'extends' => 'frame',
'x' => 75,
'y' => 150,
'width' => 150,
},
}
)
end
it 'applies multiple extends in a single rule' do
layout = Squib::LayoutParser.load_layout(layout_file('multi-extends-single-entry.yml'))
expect(layout).to eq({'aunt' => {
'a' => 101,
'b' => 102,
'c' => 103,
},
'uncle' => {
'x' => 104,
'y' => 105,
'b' => 106,
},
'child' => {
'extends' => ['uncle','aunt'],
'a' => 107, # my own
'b' => 102, # from the younger aunt
'c' => 103, # from aunt
'x' => 108, # my own
'y' => 105, # from uncle
},
}
)
end
it 'applies multi-level extends' do
layout = Squib::LayoutParser.load_layout(layout_file('multi-level-extends.yml'))
expect(layout).to eq({'frame' => {
'x' => 38,
'y' => 38,
},
'title' => {
'extends' => 'frame',
'x' => 38,
'y' => 50,
'width' => 100,
},
'subtitle' => {
'extends' => 'title',
'x' => 38,
'y' => 150,
'width' => 100,
},
}
)
end
it 'fails on a self-circular extends' do
file = layout_file('self-circular-extends.yml')
expect { Squib::LayoutParser.load_layout(file) }
.to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
end
it 'fails on a easy-circular extends' do
file = layout_file('easy-circular-extends.yml')
expect { Squib::LayoutParser.load_layout(file) }
.to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
end
it 'hard on a easy-circular extends' do
file = layout_file('hard-circular-extends.yml')
expect { Squib::LayoutParser.load_layout(file) }
.to raise_error(RuntimeError, 'Invalid layout: circular extends with \'a\'')
end
it 'redefines keys on multiple layouts' do
a = layout_file('multifile-a.yml')
b = layout_file('multifile-b.yml')
layout = Squib::LayoutParser.load_layout([a, b])
expect(layout).to eq({
'title' => { 'x' => 300 },
'subtitle' => { 'x' => 200 },
'desc' => { 'x' => 400 }
})
end
it 'evaluates extends on each file first' do
a = layout_file('multifile-extends-a.yml')
b = layout_file('multifile-extends-b.yml')
layout = Squib::LayoutParser.load_layout([a, b])
expect(layout).to eq({
'grandparent' => { 'x' => 100 },
'parent_a' => { 'x' => 110, 'extends' => 'grandparent' },
'parent_b' => { 'x' => 130, 'extends' => 'grandparent' },
'child_a' => { 'x' => 113, 'extends' => 'parent_a' },
'child_b' => { 'x' => 133, 'extends' => 'parent_b' }
})
end
it 'loads nothing on an empty layout file' do
layout = Squib::LayoutParser.load_layout(layout_file('empty.yml'))
expect(layout).to eq({})
end
it 'handles extends on a rule with no args' do
layout = Squib::LayoutParser.load_layout(layout_file('empty-rule.yml'))
expect(layout).to eq({
'empty' => nil
})
end
it 'logs an error when a file is not found' do
expect(Squib.logger).to receive(:error).once
Squib::LayoutParser.load_layout('yeti')
end
end
Loading…
Cancel
Save