Skip to content

Instantly share code, notes, and snippets.

@dimanyc
Created June 7, 2017 05:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dimanyc/f64abfc88622a025687ba16af3f3e85c to your computer and use it in GitHub Desktop.
Save dimanyc/f64abfc88622a025687ba16af3f3e85c to your computer and use it in GitHub Desktop.
Node class
### Node Class
using MemberUniquelizer
class Node < HashWithIndifferentAccess
attr_reader :id
# constructs a new hash based on injected
# headers and attributes
def self.from_rows(headers, attributes)
headers = uniqualize_headers(headers)
raise MismatchedArguments if mismatched_attributes?(headers, attributes)
raise MissingAttributeNames if has_empty_headers?(headers)
contruct_node(headers, attributes)
end
# checks if the number of headers is equal
# to the number of provided attributes
def self.mismatched_attributes?(headers, attributes)
headers.count != attributes.count
end
# checks if headers contain empties / nils
def self.has_empty_headers?(headers)
headers.count != headers.compact.count
end
# constructs a new node Hash instance
def self.contruct_node(headers, attributes)
node = new(id: id)
headers.map do |attr_name|
node[attr_name] = attributes[headers.index(attr_name)]
end
node
end
# appends unique tail to duplicate headers
# - uses MemberUniquelizer refinement:
# - app/refinements/member_uniqualizer.rb
def self.uniqualize_headers(headers)
headers.uniquelize
end
# merges a list of supplied attributes
# under a single header
def merge_attributes(new_header, attribute_headers)
self[new_header] = values_at(*attribute_headers)
end
# joins members with provided delimited
def concatinate_attribute_value(attr_name, delimiter = '|')
new_value = [].push(self[attr_name]).flatten
new_value.join(delimiter)
end
# creates a key-value style attribute
# useful for creating Shopify size and price tags
def keyvalualize_attribute(attr_name)
self[attr_name] = "#{attr_name}: #{self[attr_name]}"
end
# deletes attributes not listed
# in the 'allowed_names' list
def filter_by_attribute_names(allowed_names)
delete_by_name(allowed_names, 'key')
end
# deletes attributes not listed
# in the 'allowed_values' list
def filter_by_attribute_values(allowed_values)
delete_by_value(allowed_values, 'value')
end
# aliases Hash#keys
def attribute_names
keys.map(&:to_s)
end
# aliases Hash#values
def attribute_values
values.map(&:to_s)
end
def self.id
Time.now.to_i
end
private
# dynamically defines delete_by_[attr] methods
['name', 'value'].each do |anchor|
define_method("delete_by_#{anchor}") do |allowed_attrs, argument|
attrs_array = [].push(allowed_attrs).flatten
whitelisted_terms = attrs_array & self.send(argument.pluralize)
raise MissingAttributeNames if whitelisted_terms.empty?
self.keep_if do |attr_key, attr_value|
eval("attr_#{argument}").in?(attrs_array)
end
end
end
end
#----------------------------------------------------------------------#
### Node spec
require 'rails_helper'
RSpec.describe Node do
it { is_expected.to be_a_kind_of(Node) }
it { is_expected.to be_a_kind_of(HashWithIndifferentAccess) }
describe 'contructng new node' do
it 'should construct a new node based on lists of attributes and headers' do
expect(Node.from_rows(['foo','bar','baz'],
['fiz','biz','diz']))
.to include({ 'foo' => 'fiz',
'bar' => 'biz',
'baz' => 'diz' })
end
it 'should append a unique id to each node' do
expect(Node.from_rows(['foo','bar','baz'],
['fiz','biz','diz']).keys)
.to include('id')
end
it 'should generate unique id based on current time in seconds' do
allow(Time).to receive(:now)
.and_return('2017-06-07 00:49:03 -0400'.to_time)
node = Node.from_rows(['foo'], ['bar'])
expect(node[:id])
.to eq(1496810943)
end
it 'should return its current id' do
node = Node.from_rows(['foo'], ['bar'])
expect(node)
.to respond_to(:id)
end
it 'should create new instance of a Node class' do
expect(Node.from_rows(['foo','bar','baz'],
['fiz','biz','diz']))
.to be_a_kind_of(Node)
end
context 'when the number of headers and attributes provided does not match' do
let(:headers) { ['foo', 'bar', 'baz'] }
let(:attrs) { ['fizz', 'bizz'] }
it 'should identify that the mismatch exists' do
expect(Node.send(:mismatched_attributes?,
headers,
attrs))
.to be_truthy
end
it 'should raise MismatchedArguments error' do
expect { Node.from_rows(headers, attrs) }
.to raise_error(MismatchedArguments)
end
end # when headers.count <=> attrs.count
context 'when supplied headers contain duplicates' do
it 'should append a sequential number' do
headers = ['a', 'a', 'b']
expect(Node.send(:uniqualize_headers, headers))
.to include('a_repeat1', 'a_repeat2', 'b')
end
end # when duplicates
context 'when formatted headers contain empty names' do
it 'should identify that empty headers are present' do
expect(Node.send(:has_empty_headers?, ['a', nil, 'c']))
.to be_truthy
end
it 'should raise MissingAttributeNames error' do
headers = ['a', nil]
attrs = [1, 2]
expect { Node.from_rows(headers, attrs) }
.to raise_error(MissingAttributeNames)
end
end # when headers.contain?(nil)
end # new node
describe 'merging attribute together' do
let(:node) { Node.new(foo: 'bar', fizz: 'bizz', fuzz: 'buzz') }
it 'should append a new attribute name to node' do
node.merge_attributes('foobar', ['foo', 'fizz'])
expect(node.attribute_names)
.to include('foobar')
end
it 'should append merged values to node' do
node.merge_attributes('foobar', ['foo', 'fizz'])
expect(node['foobar'])
.to match(['bar', 'bizz'])
end
end # attr merging
describe 'concatinating values inside an attribute' do
let(:node) { Node.new(a: ['foo', 'bar', 'baz']) }
it 'should join members into a single line of text' do
expect(node.concatinate_attribute_value('a'))
.to eq('foo|bar|baz')
end
context 'when provided attribute has a single value' do
let(:node) { Node.new(foo: 'bar', fizz: 1) }
it 'should return a text value' do
expect(node.concatinate_attribute_value('foo'))
.to be_a_kind_of(String)
end
it 'should return the original attribute value' do
expect(node.concatinate_attribute_value('foo'))
.to eq('bar')
expect(node.concatinate_attribute_value('fizz'))
.to eq('1')
end
end # when not Array
end # concating array members
describe 'appending attribute name to attribute value' do
it 'should create a key-value style string from attr names and values' do
node = Node.new(foo: 'bar')
expect(node.keyvalualize_attribute('foo'))
.to eq('foo: bar')
end
end # turning value-only into key-value style string
describe 'filtering by attribute names' do
it 'should remove attributes not listed in supplied allowed_names list' do
node = Node.new(thome: 'york', aphex: 'twin', nikki: 'minaj')
names = %w(thome aphex)
node.filter_by_attribute_names(names)
expect(node.attribute_names)
.to include('thome')
expect(node.attribute_names)
.to include('aphex')
expect(node.attribute_names)
.to_not include('nikki')
expect(node.attribute_values)
.to_not include('minaj')
end
context 'when none of the current attributes match allowed_names list ' do
it 'should raise MissingAttributeNames error' do
node = Node.new(flying: 'lotus')
allowed_attributes = %w(foo bar)
expect { node.filter_by_attribute_names(allowed_attributes) }
.to raise_error(MissingAttributeNames)
end
end
context 'when allowed attributes is not a list' do
it 'should filter attributes based on the single value' do
node = Node.new(thome: 'york', nikki: 'minaj')
node.filter_by_attribute_names('thome')
expect(node.attribute_names)
.to include('thome')
expect(node.attribute_names)
.to_not include('nikki')
expect(node.attribute_values)
.to_not include('minaj')
end
end # when args are not Array
end # filtering by keys
describe 'filtering by attribute values' do
it 'should remove attributes not listed in supplied allowed_values list' do
node = Node.new(thome: 'york', aphex: 'twin', nikki: 'minaj')
node.filter_by_attribute_values(%W(york twin))
expect(node.attribute_names)
.to_not include('nikki')
expect(node.attribute_values)
.to_not include('minaj')
end
context 'when none of the current attributes match allowed attributes' do
it 'should raise MissingAttributeNames error' do
node = Node.new(flying: 'lotus')
allowed_attributes = %w(foo bar)
expect { node.filter_by_attribute_values(allowed_attributes) }
.to raise_error(MissingAttributeNames)
end
end
context 'when allowed attributes is not a list' do
it 'should filter attributes based on the single value' do
node = Node.new(thome: 'york', nikki: 'minaj')
node.filter_by_attribute_values('york')
expect(node.attribute_names)
.to include('thome')
expect(node.attribute_names)
.to_not include('nikki')
expect(node.attribute_values)
.to_not include('minaj')
end
end # when args are not Array
end # filtering by values
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment