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
This commit is contained in:
Andy Meneely
2015-06-08 13:36:23 -04:00
parent 6c60f21bb0
commit c1e410fe72
8 changed files with 341 additions and 14 deletions
+7 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+5 -1
View File
@@ -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.
#
+3 -5
View File
@@ -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