Created
June 7, 2017 05:01
-
-
Save dimanyc/f64abfc88622a025687ba16af3f3e85c to your computer and use it in GitHub Desktop.
Node class
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### 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