-
Notifications
You must be signed in to change notification settings - Fork 545
Change Package interface for ActiveShipping 2.0 #469
Conversation
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.
This is really really good.
A number of style comments. A couple bugs, that you should expand tests to cover.
But in general I strongly agree with this new interface. So much cleaner, simpler, and easier to understand.
lib/active_shipping/package.rb
Outdated
@@ -1,118 +1,62 @@ | |||
module ActiveShipping #:nodoc: | |||
class Package | |||
cattr_accessor :default_options | |||
attr_reader :options, :value, :currency | |||
cattr_accessor :default_options do {} end |
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.
Prefer:
cattr_accessor: default_options
self.default_options = {}
lib/active_shipping/package.rb
Outdated
@oversized = options[:oversized] ? true : false | ||
@unpackaged = options[:unpackaged] ? true : false | ||
def dimensions | ||
[@length, @width, @height] |
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.
This can be memoized too, if this object is correctly immutable.
lib/active_shipping/package.rb
Outdated
@oversized | ||
def volume | ||
@volume ||= if cylinder? | ||
Math::PI * width.scale(0.5).value * height.scale(0.5).value * length.value |
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.
👌 dat-scale
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.
But, you're switching to value
here. And that makes the incorrect assumption that unit
is the same on all of these.
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.
Two things:
First, would the measured gem value from a function to coerce a list of values into the same unit? For example, this would be written as:
Measured.coerce_unit(width.scale(0.5), height.scale(0.5), length).reduce(&:*)
We could also add a function to do the inner product of the values until we support multiplying these together.
Second, I would recommend parentheses around the three terms on the right, because the first multiply will coerce things into a float and then you'll lose some precision gained from :value
being Rational
or BigDecimal
. The other option would be to create a Rational
or BigDecimal
PI constant in this class and use that instead (recommended). The nice thing about that is that it will reduce the number of return value types.
lib/active_shipping/package.rb
Outdated
end | ||
|
||
def cylinder? | ||
@cylinder | ||
@cylinder ||= @options[:cylinder].present? |
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.
This is then saying cylinder: "false"
means that it is a cylinder.
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.
Not necessarily wrong, but as long as we're ok with that. And document it. And are consistent.
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.
Don't feel strong about this but I think that's an odd behaviour if someone is verbose about their options. I don't like using options hash where we largely leverage key/value but in some cases rely on the solely the key as a flag.
I'd recommend we should promote something other usage like:
options = {
...
shape: :cylinder
}
Then this becomes:
def cylinder?
@cylinder ||= @options[:shape] == :cylinder
end
Maybe shape isn't the best word, maybe package_type
or type
or mail_piece_shape
. It also means we avoid where causes like
options: {
cylinder: 'false',
envelope: 'false',
}
Also are we better to talk about box
and tube
which are pretty common packing types versus cylinder
?
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.
Great insight. I'm okay with shape
, a Package generally has a shape - box, tube, or envelope. I won't enforce the enumeration of the shape, but I'll keep the boolean methods for common shapes e.g. box/tube/envelope.
@weight = Measured::Weight(500, :grams) | ||
@length = Measured::Length(30, :cm) | ||
@width = Measured::Length(25, :cm) | ||
@height = Measured::Length(15, :cm) |
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.
Switch one of these to another unit. That would have failed the .value
bug I pointed out above.
test/unit/package_test.rb
Outdated
refute_predicate package, :tube? | ||
refute_predicate package, :oversized? | ||
refute_predicate package, :unpackaged? | ||
refute_predicate package, :gift? |
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.
Thanks for changing to _predicate
# This should return a Measured::Volume, but that doesn't exist yet. | ||
# So we'll return a raw value like we have in the past until that is a thing. | ||
test "#volume returns the volume for cylindrical package" do | ||
skip "Requires Measured 2.0" |
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.
if Gem::Version.new(Measured::VERSION) < Gem::Version.new("2.0.0.pre1")
skip "Requires Measured 2.0"
end
lib/active_shipping/package.rb
Outdated
@oversized | ||
def volume | ||
@volume ||= if cylinder? | ||
Math::PI * width.scale(0.5).value * height.scale(0.5).value * length.value |
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.
Two things:
First, would the measured gem value from a function to coerce a list of values into the same unit? For example, this would be written as:
Measured.coerce_unit(width.scale(0.5), height.scale(0.5), length).reduce(&:*)
We could also add a function to do the inner product of the values until we support multiplying these together.
Second, I would recommend parentheses around the three terms on the right, because the first multiply will coerce things into a float and then you'll lose some precision gained from :value
being Rational
or BigDecimal
. The other option would be to create a Rational
or BigDecimal
PI constant in this class and use that instead (recommended). The nice thing about that is that it will reduce the number of return value types.
lib/active_shipping/package.rb
Outdated
|
||
@dimensions = [dimensions].flatten.reject(&:nil?) | ||
def initialize(weight:, length:, width:, height: nil, options: {}) | ||
@options = @@default_options.merge(options.symbolize_keys) |
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.
If @@default_options
can be changed by a client of this library then I'd recommend @@default_options.merge(options).symbolize_keys
to guarantee all keys are symbols.
lib/active_shipping/package.rb
Outdated
process_dimensions | ||
end | ||
@value = self.class.cents_from(options[:value]) | ||
@currency = options[:currency] || options[:value].try(:currency) |
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.
What if I pass in the following Hash
for options:
{
value: MoneyWithCurrency.new(10, currency: :usd),
currency: :cad
}
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.
Then I'd question why the input is mismatched internally.
It's a matter of prioritizing an explicit value given for the options
hash, vs. prioritizing the currency defined with the value
object that might respond to currency.
I can see the value in either, but I favoured the input that was explicitly given.
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.
I think I would actually trust the currency attached to value
more than a given currency, but in the example I gave there's an inconsistency, so I think it would be fair to raise ArgumentError
.
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.
Fantastic start, just a few comments.
lib/active_shipping/package.rb
Outdated
end | ||
|
||
def cylinder? | ||
@cylinder | ||
@cylinder ||= @options[:cylinder].present? |
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.
Don't feel strong about this but I think that's an odd behaviour if someone is verbose about their options. I don't like using options hash where we largely leverage key/value but in some cases rely on the solely the key as a flag.
I'd recommend we should promote something other usage like:
options = {
...
shape: :cylinder
}
Then this becomes:
def cylinder?
@cylinder ||= @options[:shape] == :cylinder
end
Maybe shape isn't the best word, maybe package_type
or type
or mail_piece_shape
. It also means we avoid where causes like
options: {
cylinder: 'false',
envelope: 'false',
}
Also are we better to talk about box
and tube
which are pretty common packing types versus cylinder
?
lib/active_shipping/package.rb
Outdated
end | ||
alias_method :tube?, :cylinder? | ||
|
||
def gift? | ||
@gift | ||
@gift ||= @options[:gift].present? |
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.
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.
export_reason
is a little strangely worded to me, mostly for the export
included. Maybe mail_reason
? ship_reason
?
Actually, since this is a customs concern, export
is perfectly natural. I'll go with export_reason
unless somebody has arguments for otherwise.
lib/active_shipping/package.rb
Outdated
@centimetres ||= @dimensions.map { |m| m.convert_to(:cm).value } | ||
measurement.nil? ? @centimetres : measure(measurement, @centimetres) | ||
def oversized? | ||
@oversized ||= @options[:oversized].present? |
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.
Again I think we should look at what this is describing. It's a carrier's view of physical rating characteristics of a package.
e.g. for Canada Post it might be
options = {
rating_characteristics: [:oversized]
}
Where as for USPS it might be
options = {
rating_characteristics: [:oversized, :cubic]
Thoughts?
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.
That's a bit wordy, but best describes it since characteristics
is too vague.
I'm okay with rating_classifications
as well.
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.
Great comments, friends. Will address and re-ping.
lib/active_shipping/package.rb
Outdated
process_dimensions | ||
end | ||
@value = self.class.cents_from(options[:value]) | ||
@currency = options[:currency] || options[:value].try(:currency) |
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.
Then I'd question why the input is mismatched internally.
It's a matter of prioritizing an explicit value given for the options
hash, vs. prioritizing the currency defined with the value
object that might respond to currency.
I can see the value in either, but I favoured the input that was explicitly given.
lib/active_shipping/package.rb
Outdated
end | ||
|
||
def cylinder? | ||
@cylinder | ||
@cylinder ||= @options[:cylinder].present? |
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.
Great insight. I'm okay with shape
, a Package generally has a shape - box, tube, or envelope. I won't enforce the enumeration of the shape, but I'll keep the boolean methods for common shapes e.g. box/tube/envelope.
lib/active_shipping/package.rb
Outdated
end | ||
alias_method :tube?, :cylinder? | ||
|
||
def gift? | ||
@gift | ||
@gift ||= @options[:gift].present? |
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.
export_reason
is a little strangely worded to me, mostly for the export
included. Maybe mail_reason
? ship_reason
?
Actually, since this is a customs concern, export
is perfectly natural. I'll go with export_reason
unless somebody has arguments for otherwise.
lib/active_shipping/package.rb
Outdated
@centimetres ||= @dimensions.map { |m| m.convert_to(:cm).value } | ||
measurement.nil? ? @centimetres : measure(measurement, @centimetres) | ||
def oversized? | ||
@oversized ||= @options[:oversized].present? |
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.
That's a bit wordy, but best describes it since characteristics
is too vague.
I'm okay with rating_classifications
as well.
Had a great conversation with @mdking about rating concerns vs generic package concerns. TLDR
Will make these changes and iterate, but as always, everything is fluid and additional feedback is super welcome. |
- remove shipment concerns from package (i.e. gift/oversized/unpackaged) - default shape to box and create tube/envelope predicate methods - some code style fixes/improvements
What about this PR in the version 2.0? @jonathankwok @anthonyn60 |
3847e9c
to
448a674
Compare
I don't quite have the time to work on this, so if somebody wants to see this merged they can pick up where I left off somewhere in a new PR. |
Preface
This PR makes major opinionated changes to the interface of
ActiveShipping::Package
. Feel free to chime in with your own opinions, because these opinions are often my own and I've been known to be wrong in the past. 😛Changes
There are many changes here. Here's a list, with some more explicit details below each.
Package
is much more explicit, at the cost of an increase in verbosity.length:, width:, height:, weight:, options:
in any order. Keyword arguments!Height
.ArgumentError
except forHeight
, which is commonly omitted in the shipping industry (e.g. envelopes!). In this case, it'll default toMeasured::Length(0, :cm)
, where the unit is the same as the defined dimensions' units.units
/weight_unit_system
/dimensions_unit_system
/all the unit things are gone. We're usingMeasured
objects so we shouldn't need them.ounces
,pounds
,centimetres
,inches
, etc are all gone.dimensions
method that operates like the oldinches/centimetres
methods, returning an array of the dimensions; only inMeasured
objects, not Floats.ounces
andpounds
were mostly just arbitrary shortcuts to convertingMeasured
objects and getting their raw values. I don't believe it's necessary but I can be persuaded otherwise.This is incomplete!
There's a few things that need to be done before this can be merged.
cents_from
. Related: Fix money parsing #458Measured 2.0
so we can actually use the new Measured features.