Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom GTINs #29

Merged
merged 7 commits into from
Sep 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -111,4 +111,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