Browse Source

First foray into args class refactoring: rect

So far, I really like this approach. It allows us to push validation logic out of the Card class and into its own set of classes that can be more easily tested.

Fixes #75, but we've got a lot more work to do in getting this full scale
dev
Andy Meneely 11 years ago
parent
commit
c1e410fe72
  1. 12
      lib/squib/api/shapes.rb
  2. 135
      lib/squib/args/arg_loader.rb
  3. 54
      lib/squib/args/box.rb
  4. 25
      lib/squib/args/draw.rb
  5. 6
      lib/squib/deck.rb
  6. 8
      lib/squib/graphics/shapes.rb
  7. 108
      spec/args/box_spec.rb
  8. 7
      spec/graphics/graphics_shapes_spec.rb

12
lib/squib/api/shapes.rb

@ -1,3 +1,6 @@
require 'squib/args/box'
require 'squib/args/draw'
module Squib
class Deck
@ -23,13 +26,12 @@ module Squib
# @return [nil] intended to be void
# @api public
def rect(opts = {})
opts = needs(opts, [:range, :x, :y, :width, :height, :rect_radius, :x_radius, :y_radius,
opts = needs(opts, [:range, :rect_radius, :x_radius, :y_radius,
:fill_color, :stroke_color, :stroke_width, :layout])
box = Args::Box.new(self).load!(opts, expand_by: size, layout: layout, dpi: dpi)
draw = Args::Draw.new.load!(opts, expand_by: size, layout: layout, dpi: dpi)
opts[:range].each do |i|
@cards[i].rect(opts[:x][i], opts[:y][i], opts[:width][i], opts[:height][i],
opts[:x_radius][i], opts[:y_radius][i],
opts[:fill_color][i], opts[:stroke_color][i],
opts[:stroke_width][i])
@cards[i].rect(box[i], draw[i])
end
end

135
lib/squib/args/arg_loader.rb

@ -0,0 +1,135 @@
require 'squib/constants'
require 'squib/conf'
module Squib
# @api private
module Args
# Intended to be used a a mix-in,
# For example use see Box as an example
module ArgLoader
# Main class invoked by the client (i.e. api/ methods)
def load!(args, expand_by: 1, layout: {}, dpi: 300)
set_attributes(args: args)
expand(by: expand_by)
layout_args = prep_layout_args(args[:layout], expand_by: expand_by)
defaultify(layout_args: layout_args || [], layout: layout)
validate
convert_units
self
end
# Iterate over the args hash and create instance-level attributes for
# each parameter
# Assumes we have a hash of parameters to their default keys in the class
def set_attributes(args: args)
attributes = self.class.parameters.keys
attributes.each do |p|
instance_variable_set "@#{p}", args[p] # often nil, but ok
end
self.class.class_eval { attr_reader *(attributes) }
end
# Conduct singleton expansion
# If expanding-parameter is not already responding to
# :each then copy it into an array
#
# Assumes we have an self.expanding_parameters
def expand(by: 1)
exp_params = self.class.expanding_parameters
exp_params.each do |p|
attribute = "@#{p}"
arg = instance_variable_get(attribute)
unless arg.respond_to? :each
instance_variable_set attribute, [arg] * by #expand singleton
end
end
end
# Do singleton expansion on the layout argument as well
# Treated differently since layout is not always specified
def prep_layout_args(layout_args, expand_by: 1)
unless layout_args.respond_to?(:each)
layout_args = [layout_args] * expand_by
end
layout_args || []
end
# Go over each argument and fill it in with layout and defaults wherever nil
def defaultify(layout_args: [], layout: {})
self.class.parameters.each do |param, default|
attribute = "@#{param}"
val = instance_variable_get(attribute)
if val.respond_to? :each
new_val = val.map.with_index do |v, i|
v ||= layout[layout_args[i]][param] unless layout_args[i].nil?
v ||= default
end
instance_variable_set(attribute, new_val)
else # a non-expanded singleton
# TODO handle this case
end
end
end
# For each parameter/attribute foo we try to invoke a validate_foo
def validate
self.class.parameters.each do |param, default|
method = "validate_#{param}"
if self.respond_to? method
attribute = "@#{param}"
val = instance_variable_get(attribute)
if val.respond_to? :each
new_val = val.map.with_index{ |v, i| send(method, v, i) }
instance_variable_set(attribute, new_val)
else
instance_variable_set(attribute,send(method, val))
end
end
end
end
# Access an individual arg for a given card
# @return an OpenStruct that looks just like the mixed-in class
# @api private
def [](i)
card_arg = OpenStruct.new
self.class.expanding_parameters.each do |p|
p_val = instance_variable_get("@#{p}")
card_arg[p] = p_val[i]
end
card_arg
end
# Convert units
def convert_units(dpi: 300)
self.class.params_with_units.each do |p|
p_str = "@#{p}"
p_val = instance_variable_get(p_str)
if p_val.respond_to? :each
arr = p_val.map { |x| convert_unit(x, dpi) }
instance_variable_set p_str, arr
else
instance_variable_set p_str, convert_unit(p_val, dpi)
end
end
self
end
def convert_unit(arg, dpi)
case arg.to_s.rstrip
when /in$/ #ends with "in"
arg.rstrip[0..-2].to_f * dpi
when /cm$/ #ends with "cm"
arg.rstrip[0..-2].to_f * dpi * INCHES_IN_CM
else
arg
end
end
module_function :convert_unit
end
end
end

54
lib/squib/args/box.rb

@ -0,0 +1,54 @@
require 'squib/args/arg_loader'
module Squib
# @api private
module Args
class Box
include ArgLoader
def initialize(deck = nil)
@deck = deck
end
def self.parameters
{ x: 0, y: 0,
width: :native, height: :native,
radius: nil, x_radius: 0, y_radius: 0
}
end
def self.expanding_parameters
parameters.keys # all of them
end
def self.params_with_units
parameters.keys # all of them
end
def validate_width(arg, _i)
return arg if @deck.nil?
return @deck.width if arg == :native
arg
end
def validate_height(arg, _i)
return arg if @deck.nil?
return @deck.height if arg == :native
arg
end
def validate_x_radius(arg, i)
return radius[i] unless radius[i].nil?
arg
end
def validate_y_radius(arg, i)
return radius[i] unless radius[i].nil?
arg
end
end
end
end

25
lib/squib/args/draw.rb

@ -0,0 +1,25 @@
require 'squib/args/arg_loader'
module Squib
# @api private
module Args
class Draw
include ArgLoader
def self.parameters
{ fill_color: '#0000', stroke_color: :black, stroke_width: 2.0 }
end
def self.expanding_parameters
parameters.keys # all of them are expandable
end
def self.params_with_units
[:stroke_width]
end
end
end
end

6
lib/squib/deck.rb

@ -35,7 +35,11 @@ module Squib
:img_dir, :prefix, :text_hint, :typographer
# :nodoc:
# @api private
attr_reader :layout, :conf
attr_reader :layout, :conf, :dpi
#
# deck.size is really just @cards.size
def_delegators :cards, :size
# Squib's constructor that sets the immutable properties.
#

8
lib/squib/graphics/shapes.rb

@ -4,12 +4,10 @@ module Squib
# :nodoc:
# @api private
def rect(x, y, width, height, x_radius, y_radius, fill_color, stroke_color, stroke_width)
width = @width if width == :native
height = @height if height == :native
def rect(box, draw)
use_cairo do |cc|
cc.rounded_rectangle(x, y, width, height, x_radius, y_radius)
cc.fill_n_stroke(fill_color, stroke_color, stroke_width)
cc.rounded_rectangle(box.x, box.y, box.width, box.height, box.x_radius, box.y_radius)
cc.fill_n_stroke(draw.fill_color, draw.stroke_color, draw.stroke_width)
end
end

108
spec/args/box_spec.rb

@ -0,0 +1,108 @@
require 'spec_helper'
require 'squib/args/box'
describe Squib::Args::Box do
subject(:box) { Squib::Args::Box.new }
let(:expected_defaults) { {x: [0], y: [0], width: [:native], height: [:native] } }
it 'intitially has no params set' do
expect(box).not_to respond_to(:x, :y, :width, :height)
end
it 'extracts the defaults from Box on an empty hash' do
box.load!({})
expect(box).to have_attributes(expected_defaults)
end
it 'extracts what is specified and fills in defaults from Box' do
box.load!(x: 4, width: 40)
expect(box).to have_attributes(x: [4], width: [40], y: [0], height: [:native])
end
it 'extracts the defaults from Box on an empty hash' do
box.load!({foo: :bar})
expect(box).to have_attributes(expected_defaults)
expect(box).not_to respond_to(:foo)
end
context 'single expansion' do
let(:args) { {x: [1, 2], y: 3} }
before(:each) { box.load!(args, expand_by: 2) }
it 'expands box' do
expect(box).to have_attributes({
x: [1, 2],
y: [3, 3],
height: [:native, :native],
width: [:native, :native]
})
end
it 'gives access to each card too' do
expect(box[0]).to have_attributes({
x: 1,
y: 3,
height: :native,
width: :native
})
end
end
context 'layouts' do
let(:layout) do
{ 'attack' => { x: 50 },
'defend' => { x: 60 } }
end
it 'are used when not specified' do
args = { layout: ['attack', 'defend'] }
box.load!(args, expand_by: 2, layout: layout)
expect(box).to have_attributes(
x: [50, 60], # set by layout
y: [0, 0], # Box default
)
end
it 'handle single expansion' do
args = { layout: 'attack' }
box.load!(args, expand_by: 2, layout: layout)
expect(box).to have_attributes(
x: [50, 50], # set by layout
y: [0, 0], # Box default
)
end
end
context 'unit conversion' do
it 'converts units on all args' do
args = {x: ['1in', '2in'], y: 300, width: '1in', height: '1in'}
box.load!(args, expand_by: 2)
expect(box).to have_attributes(
x: [300, 600],
y: [300, 300],
width: [300, 300],
height: [300, 300],
)
end
end
context 'validation' do
it 'replaces with deck width and height' do
args = {width: :native, height: :native}
deck = OpenStruct.new(width: 123, height: 456)
box = Squib::Args::Box.new(deck)
box.load!(args, expand_by: 1)
expect(box).to have_attributes(width: [123], height: [456])
end
it 'has radius override x_radius and y_radius' do
args = {x_radius: 1, y_radius: 2, radius: 3}
box.load!(args, expand_by: 2)
expect(box).to have_attributes(x_radius: [3, 3], y_radius: [3, 3])
end
end
end

7
spec/graphics/graphics_shapes_spec.rb

@ -32,9 +32,10 @@ describe Squib::Card do
expect(cxt).to receive(:restore).once
card = Squib::Card.new(deck, 100, 150)
# rect(x, y, width, height, x_radius, y_radius,
# fill_color, stroke_color, stroke_width)
card.rect(37, 38, 50, 100, 10, 15, '#fff', '#f00', 2.0)
# rect(Args::Box, x_radius, y_radius, Args::Draw)
box = OpenStruct.new(x: 37, y: 38, width: 50, height: 100, x_radius: 10, y_radius: 15)
draw = OpenStruct.new(fill_color: '#fff', stroke_color: '#f00', stroke_width: 2.0)
card.rect(box, draw)
end
end

Loading…
Cancel
Save