-
Notifications
You must be signed in to change notification settings - Fork 2
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
Conversation
This refactors GTIN handler class lookup so it can be extended without monkeypatching. Developers can add/remove classes in `BarcodeValidation::GTIN.gtin_classes` to control which class wraps a GTIN. The use case I need this for is internall dummy GTINs that are longer than any of the standard ones. The Readme is updated to describe the feature.
@beet could you please also have a look at this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interested in the use-case for implementing non-standard GTIN lengths, otherwise something like the following would allow us to simply implement any class under BarcodeValidation::GTIN
, like BarcodeValidation::GTIN::MyCustomGTIN
, without having to do anything else like unshifting constants into an array:
module BarcodeValidation
module GTIN
...snip...
private
def class_for_input(input)
concrete_implementations.find do |klass|
input.to_s.size == klass::VALID_LENGTH
end
end
def concrete_implementations
subclasses.select(&:concrete_implementation?)
end
def subclasses
constants.map do |class_name|
const_get(class_name)
end
end
def concrete_implementation?
false
end
end
end
end
module BarcodeValidation
module GTIN
class Base < BarcodeValidation::DigitSequence
...snip...
def self.concrete_implementation?
self.const_defined?(:VALID_LENGTH)
end
end
end
end
module BarcodeValidation
module GTIN
class CheckDigit < DelegateClass(Digit)
...snip...
def self.concrete_implementation?
false
end
end
end
end
Could always override #valid?
in the custom GTIN implementation for rules like returning true for all inputs of the valid length that start with "123" kinda thing:
module BarcodeValidation
module GTIN
class MyCustomGTIN < BarcodeValidation::GTIN::Base
VALID_LENGTH = 20
def valid?
input.start_with?("123") && super
end
end
end
end
Subclasses like you indicate would be a way to do it, but the downside is that it sort of leaves the ordering of those classes undefined. This is fine if length is the only distinction, but in our case we have specialized GTINs for a subset of data for a particular length, with the default classes being a fallback. For example: we're running into barcode normalizing behaviors which are different in a few countries we process barcodes from, specifically where supermarket barcodes can embed weight and price information for a weighed piece of fruit. These normalization rules vary by country and we're looking at moving this logic into our own GTIN classes so we can call This means we'd like to put our country-specific GTIN classes in the We're still in the process of sorting normalization out ourselves, which is why we figured having more control over the loading order like this PR implements allows us to experiment with it. I imagine that implementations of barcode normalization could be interesting for a future PR, if you're interested. |
Thanks for explaining the concrete use-case. Is there a way to achieve non-standard lengths without exposing internal implementation details of This feels too tightly coupled: From the pseudo code above, if we abstract module BarcodeValidation
module GTIN
class MyCustomGTIN < BarcodeValidation::GTIN::Base
VALID_LENGTH = 20
def self.handles?(input)
input.start_with?("123") && input.length <= VALID_LENGTH
end
def valid?
input.start_with?("123") && super
end
end
end
end |
I understand the concern about directly exposing the implementation detail of With some modifications your original self-registering subclasses can probably work very well: module BarcodeValidation
module GTIN
class MyCustomGTIN13 < BarcodeValidation::GTIN::GTIN13
# We address a subset of GTIN13's range, so ask this class before that one if it handles a GTIN
prioritize_before BarcodeValidation::GTIN::GTIN13
# Custom implementations for handles/valid to scope the subset of data this one cares about
def self.handles?(input)
input.start_with?("123") && super
end
def valid?
input.start_with?("123") && super
end
end
end
end The API I'm thinking of for this adds two class-level methods:
I've got some ideas for the implementation, so I'll probably pick it up over the weekend or next week. That gives a small window of opportunity to give feedback on the concepts before they are code 😄 |
This is part of marketplacer#29, which makes it easier to implement custom GTINs as a user of this library. The first implementation in a0d0e9b worked, but required the user to directly manipulate registered classes inside an Array. As @beet pointed out, it's better to not expose implementation details like that and look for a more elegant interface. This commit changes the implementation by replacing `gtin_classes` with `prioritized_gtin_classes` and adding a note that this is a private API. Any subclass of `GTIN::Base` will be automatically appended to this list. The user gets two tools directly inside `GTIN::Base`: - `prioritize_before (other_class)`: moves this class to directly in front of the other class. This allows this class to be asked if it `handles?` a GTIN before the other class. - `abstract_class`: lets a class remove itself from the list of classes that are asked if they handle a GTIN. There are tests for both the implementation details and functionality.
@beet What do you think of the new implementation? There are now tests as well, because that's one thing that was also missing in my initial implementation due to it being more of a refactoring that just exposed internals as a side effect. |
Reflection: it feels like managing class lookups is probably a concern that deserves its own class to manage the internal details and expose a clean interface that other classes can use. Right now GTIN::Base is manipulating the internals of GTIN itself, which feels a little icky. I just wanted to work out an implementation for the proposed public API so if we agree on this, I can refine it instead of wasting my time polishing a dismissed idea 😅 |
The goal is to make `prioritized_gtin_classes` private data and only expose methods that have interactions with it. Replacing #delete behavior from GTIN::Base with calls to this new method is one step in the right direction.
This is another small step towards making GTIN::Base not manipulate the internals of `GTIN.prioritized_gtin_classes`, but letting GTIN handle that by itself.
Manipulating `GTIN.prioritized_gtin_classes` now only happens through helper methods on GTIN, theoretically allowing the list itself to become private.
Having a few lines to outline the purpose of a class and its neighbours tends to help understanding and exploration of a codebase.
The only thing that still depended on it being public were the tests for functionality that manipulated the list. The tests have been overhauled to rely more on asserting behavior than internal state.
I've cleaned up the implementation by moving all direct manipulations of the GTIN list into methods in I considered making What do you think of this implementation? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one, thanks @Narnach, very neat!
We almost have a command chain happening now with a means of explicitly positioning a handler before another in the chain, very flexible.
Can look into doing a release next week.
@Narnach I tried doing a release, but Rubocop took offence at the build: https://buildkite.com/marketplacer/barcodevalidation/builds/52 Don't know why the PR was green, but the release failed... |
Rubocop checks in PR marketplacer#29 did not fire, so issues were not discovered until release. This corrects them, making Rubocop and tests pass. - GTIN module was 31 lines (max 30), so I inlined a guard clause - Use of redundant `self.` was removed - Prefer `__send__` over `send` when `public_send` is not an option
Sorry! I also relied on the PR green checkbox working as intended... 👀 It may be that the checks don't kick in for PRs from external contributors. I've seen this happen on client projects where I started off with PRs from my fork of their project. Github Actions has some options to try and make it work, but for an external platform like buildkite I can imagine they won't do anything unless you're in the organization itself. That said, I've create a PR to address the issues. |
Support custom GTINs by adding their class to
BarcodeValidation::GTIN.gtin_classes
This refactors GTIN handler class lookup so it can be extended without monkeypatching. Developers can add/remove classes in
BarcodeValidation::GTIN.gtin_classes
to control which class wraps a GTIN.The use case I need this for is internal dummy GTINs that are longer than any of the standard ones and have additional validation rules besides just length.
The Readme is updated to describe the feature.