Cast AccountNumber string to integer#158
Cast AccountNumber string to integer#158samuel02 wants to merge 3 commits intoaccodeing:developmentfrom
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.
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.
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.
@samuel02 Why dropping this case? Is it not needed anymore?
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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? }
| .constrained(gteq: 0, lteq: 9999) | ||
| .optional | ||
| .constructor do |input| | ||
| unless input.nil? || input.to_s.empty? |
| AccountNumber = Strict::Int | ||
| .constrained(gteq: 0, lteq: 9999) | ||
| .optional | ||
| .constructor do |input| |
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.