Skip to content

Commit

Permalink
Merge pull request #29 from Narnach/support-custom-gtin-classes
Browse files Browse the repository at this point in the history
Support custom GTINs
  • Loading branch information
beet authored Sep 28, 2022
2 parents 21202d9 + 284c50d commit aec8119
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 9 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,46 @@ bad.to_all_valid # => []
```


Custom GTINs
------------

If the standard GTINs provided are not enough for your needs, you can implement your own by subclassing `BarcodeValidation::GTIN::Base` or any of its subclasses. If your custom class overlaps with a default class or one of your other custom classes, you can declare `prioritize_before <other class>` to re-order their evaluation order.

An example:

```ruby
# A custom class that handles any length GTIN as long as it starts with "123".
# Note that we must still provide a VALID_LENGTH to allow transcoding to other GTINs by zero-padding.
# Due to this inheriting from Base, it is automatically registered and added to the end of the list of classes to check if it `handles?` an input.
class MyCustomGTIN < BarcodeValidation::GTIN::Base
VALID_LENGTH = 20

def self.handles?(input)
input.start_with?("123") && input.length <= VALID_LENGTH
end

# Custom validity check
def valid?
self.class.handles?(input) && check_digit.valid?
end
end

# A custom implementation of GTIN13, which addresses a subset of the GTIN13 range.
class MyCustomGTIN13 < BarcodeValidation::GTIN::GTIN13
# Ensure we get a chance to handle GTINs before our parent,
# so we can handle the subset we care about and have our parent handle the rest.
prioritize_before BarcodeValidation::GTIN::GTIN13

def self.handles?(input)
input.start_with?("123") && super
end

def valid?
input.start_with?("123") && super
end
end
```


Development
-----------
Expand Down
55 changes: 46 additions & 9 deletions lib/barcodevalidation/gtin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,64 @@

require "forwardable"
require_relative "invalid_gtin"
require_relative "gtin/base"
require_relative "gtin/check_digit"
require_relative "gtin/gtin8"
require_relative "gtin/gtin12"
require_relative "gtin/gtin13"
require_relative "gtin/gtin14"

module BarcodeValidation
# GTIN is responsible for wrapping input in an appropriate GTIN::Base sub-class.
# An important part of this involves managing the prioritized list of GTIN classes we use for handling input.
# The methods implemented here are used by GTIN::Base to manage this list and prioritize classes.
module GTIN
class << self
def new(input)
(class_for_input(input) || BarcodeValidation::InvalidGTIN).new(input)
end

# Adds the provided class to the back of the list of prioritized GTIN classes.
def append_gtin_class(gtin_class)
return if gtin_class?(gtin_class)

prioritized_gtin_classes.push(gtin_class)
nil
end

# Ensure the provided class is removed from the list of prioritized GTIN classes.
def remove_gtin_class(gtin_class)
prioritized_gtin_classes.delete(gtin_class)
nil
end

# Is this a registered prioritized GTIN class?
# @return [true, false]
def gtin_class?(gtin_class)
prioritized_gtin_classes.include?(gtin_class)
end

# @param [Class] high_priority_class The higher priority GTIN class you want to move before the low priority class
# @param [Class] low_priority_class The low priority GTIN class that the high priority one is moved before
def reprioritize_before(high_priority_class, low_priority_class)
low_priority_index = prioritized_gtin_classes.index(low_priority_class)
remove_gtin_class(high_priority_class)
prioritized_gtin_classes.insert(low_priority_index, high_priority_class)
nil
end

private

def prioritized_gtin_classes
@prioritized_gtin_classes ||= []
end

def class_for_input(input)
[GTIN8, GTIN12, GTIN13, GTIN14].find do |klass|
input.to_s.size == klass::VALID_LENGTH
end
input = input.to_s.freeze
prioritized_gtin_classes.find { |klass| klass.handles?(input) }
end
end
end
end

# Load GTIN implementations after we have our registration setup
require_relative "gtin/base"
require_relative "gtin/check_digit"
require_relative "gtin/gtin8"
require_relative "gtin/gtin12"
require_relative "gtin/gtin13"
require_relative "gtin/gtin14"
28 changes: 28 additions & 0 deletions lib/barcodevalidation/gtin/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,34 @@ def initialize(input)
BarcodeValidation::InvalidGTIN.new(input, error: e)
end

# Does this class (potentially) handle a GTIN that matches the input?
# Subclasses can choose to implement their own logic. The default is to look at +VALID_LENGTH+ and use that to match the length of the input the class handles.
def self.handles?(input)
return false unless self.const_defined?(:VALID_LENGTH)

input.length == self::VALID_LENGTH
end

# Upon inheritance, register the subclass so users of the library can dynamically add more GTINs in their own code.
def self.inherited(subclass)
BarcodeValidation::GTIN.append_gtin_class(subclass)
end

# Ensure this class is earlier in the GTIN classes list than +other_gtin_class+ and thus will get asked earlier if it handles a GTIN.
def self.prioritize_before(other_gtin_class)
raise ArgumentError, "The class you want to prioritize before is not a registered prioritized GTIN class." unless GTIN.gtin_class?(other_gtin_class)

GTIN.reprioritize_before(self, other_gtin_class)
end

# This class is abstract and should not be included in the list of GTIN classes that actually implement a GTIN.
def self.abstract_class
BarcodeValidation::GTIN.remove_gtin_class(self)
end

# GTIN::Base is an abstract class. See GTIN8/12/13/14 for implementations of actual GTINs.
abstract_class

def valid?
valid_length == length && check_digit.valid?
end
Expand Down
71 changes: 71 additions & 0 deletions spec/lib/barcodevalidation/gtin_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,75 @@
end
end
end

describe "custom classes" do
after(:each) do
# Remove references to classes we may have generated during a test to ensure we don't pollute the list of classes for other tests
classes_to_cleanup.each do |class_sym|
next unless Object.const_defined?(class_sym)

class_obj = Object.const_get(class_sym)
BarcodeValidation::GTIN.remove_gtin_class(class_obj)
Object.send(:remove_const, class_sym)
end
end

describe "inheritance" do
let(:classes_to_cleanup) { [:MyCustomGTIN] }

it "appends itself to BarcodeValidation::GTIN.prioritized_gtin_classes when inheriting from GTIN::Base" do
class MyCustomGTIN < BarcodeValidation::GTIN::Base
end

expect(BarcodeValidation::GTIN.gtin_class?(MyCustomGTIN)).to eq(true)
end

it "appends itself to BarcodeValidation::GTIN.prioritized_gtin_classes when inheriting from a GTIN::Base descendant" do
class MyCustomGTIN < BarcodeValidation::GTIN::GTIN12
end

expect(BarcodeValidation::GTIN.gtin_class?(MyCustomGTIN)).to eq(true)
end
end

describe "prioritize_before" do
let(:classes_to_cleanup) { [:MyCustomGTIN13] }

it "causes this class to be evaluated before the other one when checking which one handles a GTIN input" do
class MyCustomGTIN13 < BarcodeValidation::GTIN::GTIN13
prioritize_before BarcodeValidation::GTIN::GTIN13

def self.handles?(input)
input.start_with?("123") && super
end
end

expect(BarcodeValidation::GTIN.new("1234567890123").class).to eq(MyCustomGTIN13)
expect(BarcodeValidation::GTIN.new("1004567890123").class).to eq(BarcodeValidation::GTIN::GTIN13)
end
end

describe "abstract_class" do
let(:classes_to_cleanup) { %i[MyCustomAbstractGTIN MyCustomParentGTIN MyCustomChildGTIN] }

it "removes itself from BarcodeValidation::GTIN.prioritized_gtin_classes" do
class MyCustomAbstractGTIN < BarcodeValidation::GTIN::Base
abstract_class
end

expect(BarcodeValidation::GTIN.gtin_class?(MyCustomAbstractGTIN)).to eq(false)
end

it "is not inherited by children" do
class MyCustomParentGTIN < BarcodeValidation::GTIN::Base
abstract_class
end

class MyCustomChildGTIN < MyCustomParentGTIN
end

expect(BarcodeValidation::GTIN.gtin_class?(MyCustomChildGTIN)).to eq(true)
end
end
end
end

0 comments on commit aec8119

Please sign in to comment.