-
Notifications
You must be signed in to change notification settings - Fork 8
Cast AccountNumber string to integer #158
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
base: development
Are you sure you want to change the base?
Cast AccountNumber string to integer #158
Conversation
lib/fortnox/api/models/customer.rb
Outdated
|
|
||
| # SalesAccount Sales account of the customer, 4 digits | ||
| attribute :sales_account, Types::AccountNumber | ||
| attribute :sales_account, Types::Sized::String[4] |
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 not ok as is as it would allow the account NUMBER to be set to "abcd" etc. If the API actually barfs if we send it a number here we should do some casting instead.
Is this something you have experienced problems with or was it an attempt to prevent problems? Also note that we do NOT reflect the Fortnox API spec directly in our API due to theirs being full of errors, conflicts and inconsistencies. Instead we try to normalise the API as much as possible, returning actual Ruby Date objects for date and time, always representing numeric types as numbers, using enums in place of strings for countries etc.
97c686c to
3f8f142
Compare
3f8f142 to
d6c099b
Compare
ehannes
left a comment
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! Please have a look my comments. Feel ask if something is unclear or if I have misunderstood something.
| it { is_expected.to be_nil } | ||
| end | ||
|
|
||
| context 'when AccountNumber created with empty string' do |
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.
@samuel02 Why dropping this case? Is it not needed anymore?
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.
It got complicated :) The problem is that the constructor Dry::Types::Coercions::Form.to_int will return nil in the case of an empty string. And since the type is declared as optional that's ok and it won't raise anything. What do you think? Should I write a custom constructor that handles that case 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.
Well, it being complicated is hardly a reason to not test for it. In fact it is a very good reason TO test for it! 😄
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.
Sure :) My thinking was just that it doesn't maybe need to be that strict. The consequence of the new code would be that an empty string would end up as a nil value which was already permitted. So in practice it probably doesn't matter that much. But sure :) I pushed another implementation. Unfortunately it's not enough with the constructor you provided since Integer doesn't implement #empty?, if we'd have ActiveSupport we could have used Object#blank? but I didn't want to pull that in. So I ended up writing my own. WDYT?
|
|
||
| context 'when AccountNumber created with valid number' do | ||
| include_examples 'equals input', 1234 | ||
| subject { klass['1234'] } |
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.
1234 should still be a valid number, right? We want to test both string and integer input. So we should probably add a when AccountNumber created with valid string case here instead of changing this integer to string.
| end | ||
|
|
||
| context 'when AccountNumber created with a too large number' do | ||
| include_examples 'raises ConstraintError', 10_000 |
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.
Same here. We should probably keep these as integers. We only need to test the casting once.
| end | ||
|
|
||
| context 'when AccountNumber created with a negative number' do | ||
| include_examples 'raises ConstraintError', -1 |
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.
Same here. We should probably keep these as integers. We only need to test the casting once.
b931c20 to
26042da
Compare
| it { is_expected.to be_nil } | ||
| end | ||
|
|
||
| context 'when AccountNumber created with empty string' do |
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.
Well, it being complicated is hardly a reason to not test for it. In fact it is a very good reason TO test for it! 😄
lib/fortnox/api/types.rb
Outdated
| AccountNumber = Strict::Int | ||
| .constrained(gteq: 0, lteq: 9999) | ||
| .optional | ||
| .constructor(Dry::Types::Coercions::Form.method(:to_int)) |
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 like the thought behind this. You can probably achieve the same result by changing Strict::Int to Coercible::Int at the top of the declaration. I'm assuming this doesn't work for the empty string case though.
To correctly deal with nil and empty string you can do something like this:
AccountNumber = Strict::Int
.constrained(gteq: 0, lteq: 9999)
.optional
.constructor { |v| Integer(v) unless v.nil? or v.empty? }
ehannes
left a comment
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.
Now we're close :)
| .constrained(gteq: 0, lteq: 9999) | ||
| .optional | ||
| .constructor do |input| | ||
| unless input.nil? || input.to_s.empty? |
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.
Are these cases tested?
| AccountNumber = Strict::Int | ||
| .constrained(gteq: 0, lteq: 9999) | ||
| .optional | ||
| .constructor do |input| |
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 constructor is now 9 lines. Can we create a separate method for this? I'm pretty sure Rubocop will complain about 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.
Look ma, one-liner ...
.constructor do |input|
input.to_s.empty? ? nil : Integer(input.to_s)
endOptimizing the logic for brevity and skipping the rescue. No one should send random user input directly at this and if they do that is their problem. We are not end user facing, we are developer facing and can expect that the input has already been sanitised. So allow for reasonable things - like nil and string/symbol versions - where it makes sense, but do not try to defend from all bad inputs.
| input | ||
| end | ||
| rescue ArgumentError | ||
| input |
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.
Is this case tested?
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 all valid/invalid cases are tested. We could test for everything but right now, with my suggested changes, we do test the two main cases: Things that respond to to_s and has a valid value will convert, the rest will cause a Dry types constraint error...
| include_examples 'raises ConstraintError', -1 | ||
| end | ||
|
|
||
| context 'when AccountNumber created with an invalid string' do |
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.
Firstly this should be describe Fortnox::API::Types::AccountNumber surely? The spec is named as specifically for that anyway :) That will also mean that we can skip the "when AccountNumber ..." for all contexts and you will have just context 'created with an invalid string' do for example.
Basically this test could be anything that we don't explicitly allow, so make it with invalid input and just make sure we get a Dry types error for now.
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.
Yes, it should be describe Fortnox::API::Types::AccountNumber. My bad!
| context 'when AccountNumber created with valid string' do | ||
| subject { klass['1234'] } | ||
|
|
||
| it 'casts it to a number' do |
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.
We don't really care HOW it does something here, just that we get the correct result. So this could be:
context 'created with valid string' do
subject { klass['1234'] }
it { is_expected.to eq 1234 }
endUtilizing the readability of RSpecs one line syntax.
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.
Actually if we consider how we do things we can change it to 'created with valid value that responds to to_s', since that is actually what we allow.
e19d08d to
bd64a28
Compare
According to the documentation at https://developer.fortnox.se/documentation/resources/customers/
the field
SalesAccounton theCustomeris a 4 digit string.