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

Add annotations for shopify-money gem #153

Merged
merged 4 commits into from
Apr 6, 2023
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
5 changes: 5 additions & 0 deletions index.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@
"rainbow/version"
]
},
"shopify-money": {
"requires": [
"money"
]
},
"sidekiq": {
"dependencies": [
"rails"
Expand Down
174 changes: 174 additions & 0 deletions rbi/annotations/shopify-money.rbi
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# typed: strong

class Money
olivier-thatch marked this conversation as resolved.
Show resolved Hide resolved
sig { returns(BigDecimal) }
attr_reader :value

sig { returns(T.any(Money::Currency, Money::NullCurrency)) }
attr_reader :currency
Comment on lines +7 to +8
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Annoyingly, Money::NullCurrency is not a subclass of Money::Currency, so we have to use a union.


sig do
params(
value: T.nilable(T.any(Money, Numeric, String)),
currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String)),
)
.void
end
def initialize(value, currency); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(T::Boolean) }
def zero?(*args, **_arg1, &block); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(T::Boolean) }
def nonzero?(*args, **_arg1, &block); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(T::Boolean) }
def positive?(*args, **_arg1, &block); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(T::Boolean) }
def negative?(*args, **_arg1, &block); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(Integer) }
def to_i(*args, **_arg1, &block); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(Float) }
def to_f(*args, **_arg1, &block); end

# @method_missing: delegated to BigDecimal
sig { params(args: T.untyped, _arg1: T.untyped, block: T.nilable(T.proc.void)).returns(Integer) }
def hash(*args, **_arg1, &block); end
Comment on lines +19 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The (*args, **_arg1, &block) arguments are necessary to appease Sorbet, because that's the signature it generates for methods delegated via def_delegators.


class << self
sig { params(block: T.nilable(T.proc.params(config: Money::Config).void)).void }
def configure(&block); end

sig do
params(
value: T.nilable(T.any(Money, Numeric, String)),
currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String)),
)
.returns(Money)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now causing errors when Money instances are multiplied with integers, 5 * Money.new as the second argument isn't an Integer https://github.com/sorbet/sorbet/blob/dd4602e030832c930d0b413f31734a83708448f6/rbi/core/integer.rbi#L59.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, that's a bummer. I can't think of an easy fix though -- we'd need to be able to overload the signatures for Integer#*, Float#*, etc. with an additional signature, and I think Sorbet only supports overloading for its own stdlib types.

Maybe this is an acceptable limitation? It's easily fixed by swapping the arguments so that Money#* is called instead. It would be nice if this could be made more obvious but I'm not sure how.

Open to any and all suggestions for improving this :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can't represent the current implementation without overloading that signature.

I'm okay with swapping the arguments solution. Only problem is it's hard to communicate this moving forward.

end
def new(value = 0, currency = nil); end

sig do
params(
subunits: T.nilable(T.any(Money, Numeric, String)),
currency_iso: T.nilable(T.any(Money::Currency, Money::NullCurrency, String)),
format: Symbol,
)
.returns(Money)
end
def from_subunits(subunits, currency_iso, format: :iso4217); end

sig { params(money1: Money, money2: Money).returns(Rational) }
def rational(money1, money2); end

sig { returns(T.nilable(T.any(Money::Currency, Money::NullCurrency, String))) }
def current_currency; end

sig { params(currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String))).void }
def current_currency=(currency); end

sig do
type_parameters(:U)
.params(
new_currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String)),
block: T.nilable(T.proc.returns(T.type_parameter(:U))),
)
.returns(T.type_parameter(:U))
end
def with_currency(new_currency, &block); end
end

sig { params(format: Symbol).returns(Integer) }
def subunits(format: :iso4217); end

sig { returns(T::Boolean) }
def no_currency?; end

sig { returns(Money) }
def -@; end

sig { params(other: T.untyped).returns(T.nilable(Integer)) }
def <=>(other); end

sig { params(other: T.untyped).returns(Money) }
def +(other); end

sig { params(other: T.untyped).returns(Money) }
def -(other); end

sig { params(numeric: Numeric).returns(Money) }
def *(numeric); end

sig { params(numeric: Numeric).returns(T.noreturn) }
def /(numeric); end
Comment on lines +111 to +112
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


sig { returns(String) }
def inspect; end

sig { params(other: T.untyped).returns(T::Boolean) }
def ==(other); end

sig { params(other: T.untyped).returns(T::Boolean) }
def eql?(other); end

sig { params(currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String))).returns(Money) }
def to_money(currency = nil); end

sig { returns(BigDecimal) }
def to_d; end

sig { params(style: T.nilable(Symbol)).returns(String) }
def to_fs(style = nil); end

sig { params(options: T.nilable(T::Hash[Symbol, T.untyped])).returns(String) }
def to_json(options = nil); end

sig { params(options: T.nilable(T::Hash[Symbol, T.untyped])).returns(T::Hash[Symbol, String]) }
def as_json(options = nil); end

sig { returns(Money) }
def abs; end

sig { returns(Money) }
def floor; end

sig { params(ndigits: Integer).returns(Money) }
def round(ndigits = 0); end

sig { params(rate: Numeric).returns(Money) }
def fraction(rate); end

sig { params(splits: T::Array[Numeric], strategy: Symbol).returns(T::Array[Money]) }
def allocate(splits, strategy); end

sig { params(maximums: T::Array[Numeric]).returns(T::Array[Money]) }
def allocate_max_amounts(maximums); end

sig { params(num: Numeric).returns(T::Array[Money]) }
def split(num); end

sig { params(num: Numeric).returns(T::Hash[Money, Numeric]) }
def calculate_splits(num); end
Comment on lines +156 to +160
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't really make sense to pass non-integers to the split and calculate_splits methods IMO, but technically the gem does support it (even though it produces surprising/arguably incorrect results with non-integers).


sig { params(min: Numeric, max: Numeric).returns(Money) }
def clamp(min, max); end
end

class Numeric
sig { params(currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String))).returns(Money) }
def to_money(currency = nil); end
end

class String
sig { params(currency: T.nilable(T.any(Money::Currency, Money::NullCurrency, String))).returns(Money) }
def to_money(currency = nil); end
end