Browse Source
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 scaledev
8 changed files with 341 additions and 14 deletions
@ -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 |
||||||
@ -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 |
||||||
@ -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 |
||||||
@ -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 |
||||||
Loading…
Reference in new issue