Skip to content
Open
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
2 changes: 1 addition & 1 deletion fortnox-api.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| # rubocop:disable Metrics/BlockLength

spec.required_ruby_version = '>= 2.3'
spec.add_dependency 'dry-struct', '~> 0.1'
spec.add_dependency 'dry-types', '~> 0.8'
spec.add_dependency 'dry-types', '~> 0.8', '< 0.13.0'
Copy link
Member

Choose a reason for hiding this comment

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

Did you mean < here or <=? This will keep it to the latest 0.12.x, unclear if that was the intention :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually meant <.

0.13.0 introduces breaking changes and I didn't want the upgrade to be part of this PR. See https://github.com/dry-rb/dry-types/blob/master/CHANGELOG.md#v0130-2018-05-03

# TODO: Temporary lockdown. See issue #103 for more info.
spec.add_dependency 'httparty', '~> 0.14.0'

Expand Down
20 changes: 20 additions & 0 deletions lib/fortnox/api/mappers/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

require 'fortnox/api/mappers/base'

module Fortnox
module API
module Mapper
class Metadata < Fortnox::API::Mapper::Base
KEY_MAP = {
total_resources: '@TotalResources',
total_pages: '@TotalPages',
current_page: '@CurrentPage'
}.freeze
JSON_ENTITY_WRAPPER = 'MetaInformation'
end

Registry.register(Metadata.canonical_name_sym, Metadata)
end
end
end
1 change: 1 addition & 0 deletions lib/fortnox/api/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
require 'fortnox/api/models/project'
require 'fortnox/api/models/unit'
require 'fortnox/api/models/terms_of_payment'
require 'fortnox/api/models/metadata'
21 changes: 21 additions & 0 deletions lib/fortnox/api/models/metadata.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'fortnox/api/types'

module Fortnox
module API
module Model
class Metadata < Model::Base
STUB = {
current_page: '',
total_resources: '',
total_pages: ''
}.freeze

attribute :current_page, Types::Required::Integer.is(:read_only)
attribute :total_resources, Types::Required::Integer.is(:read_only)
attribute :total_pages, Types::Required::Integer.is(:read_only)
end
end
end
end
6 changes: 5 additions & 1 deletion lib/fortnox/api/repositories/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
require 'fortnox/api/request_handling'
require 'fortnox/api/repositories/base/loaders'
require 'fortnox/api/repositories/base/savers'
require 'fortnox/api/repositories/base/pagination'
require 'fortnox/api/mappers/metadata'

module Fortnox
module API
Expand All @@ -13,6 +15,7 @@ class Base
include Fortnox::API::RequestHandling
include Loaders
include Savers
include Pagination

HTTParty::Parser::SupportedFormats['text/html'] = :json

Expand All @@ -24,7 +27,7 @@ class Base
HTTP_METHODS = %i[get put post delete].freeze

attr_accessor :headers
attr_reader :mapper, :keys_filtered_on_save
attr_reader :mapper, :keys_filtered_on_save, :metadata

def self.set_headers(headers = {}) # rubocop:disable Naming/AccessorMethodName
self.headers.merge!(headers)
Expand All @@ -49,6 +52,7 @@ def initialize(keys_filtered_on_save: [:url], token_store: :default)
@keys_filtered_on_save = keys_filtered_on_save
@token_store = token_store
@mapper = Registry[Mapper::Base.canonical_name_sym(self.class::MODEL)].new
@metadata_mapper = Mapper::Metadata.new
end

def next_access_token
Expand Down
14 changes: 12 additions & 2 deletions lib/fortnox/api/repositories/base/loaders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ module API
module Repository
module Loaders
def all
response_hash = get(self.class::URI)
instantiate_collection_response(response_hash)
results = []

loop do
Copy link
Member

Choose a reason for hiding this comment

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

Something like this might be a little clearer:

# In the pagination module
def pages
  (1..total_pages ).to_a # Returns an array with [1,2,...,n] where n == total_pages
end

# In this file
def all
  pages.each_with_object([]) do |page_number, results|
    response_hash = get(self.class::URI, query: { page: page_number })
    results.push( *instantiate_collection_response( response_hash ))
  end
end

That way we don't have to do bounds checking, so we can drop last_page? and get the page number for free, so we can drop next_page. And we get a cleaner method without temporary variables 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm.. I like that way that looks but that doesn't solve for the inital request to get the number of total pages.. I guess we could do one request and then only start paginating if there are more pages but that makes it kind of ugly.. Any suggestions?

Copy link
Member

@d-Pixie d-Pixie Jun 15, 2018

Choose a reason for hiding this comment

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

My updated suggestion:

def all( limit: -1 )
  # Cast and clamp to natural number
  requested_number_of_entities = Integer(limit)
  limit_imposed = requested_number_of_entities.positive?
  # Max 500 entities per request
  request_limit = limit_imposed ? [requested_number_of_entities,500].max : 500
  # Start at the first page
  page = 1
  results = []

  loop do
    response_hash = get(self.class::URI, query: { page: page, limit: request_limit })
    results.push( *instantiate_collection_response( response_hash ))

    total_pages = response_hash['MetaInformation']['@TotalPages']

    limit_reached = limit_imposed && results.length >= requested_number_of_entities
    no_more_pages = total_pages == page
    break if limit_reached or no_more_pages
    page += 1
  end

  results = results[0...requested_number_of_entities] unless requested_number_of_entities.negative?

  return results
end

Copy link
Member

Choose a reason for hiding this comment

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

[I updated it a bit after I ran it locally and got some errors]

That code works for getting all when < 500 as well as limiting the result to the requested limit. I tested pagination by setting the request_limit to 10 and making sure the query still returned the right number of entities.

The test code I used, after starting the debug console with bin/console, was this:

reload!
Fortnox::API.configure do |config|
  config.client_secret = '[SECRET]'
  config.access_token = '[UUID]'
end
r = Fortnox::API::Repository::Invoice.new
r.all.length

Sometimes with a limit: argument to the all call.

response_hash = get(self.class::URI, query: { page: next_page })
results += instantiate_collection_response(response_hash)
break if last_page?
end

results
end

def only(filter)
Expand Down Expand Up @@ -53,6 +60,9 @@ def escape(key, value)
private

def instantiate_collection_response(response_hash)
metadata_hash = @metadata_mapper.wrapped_json_hash_to_entity_hash(response_hash)
@metadata = Model::Metadata.new(metadata_hash)

entities_hash = @mapper.wrapped_json_collection_to_entities_hash(response_hash)
entities_hash.map do |entity_hash|
instantiate(entity_hash)
Expand Down
25 changes: 25 additions & 0 deletions lib/fortnox/api/repositories/base/pagination.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Fortnox
module API
module Repository
module Pagination
Copy link
Member

@d-Pixie d-Pixie Jun 15, 2018

Choose a reason for hiding this comment

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

I think we should drop this and create a proper class for the metadata if we want methods on it instead.

Right now the @metadata is an instance of the metadata model, that is fine. But the pagination shouldn't live on the repository singleton. The way me current example looks it keeps all that in the single call. Your implementation leaves things on the global repository object instead ....

A better place for it would be the collection we are building, results = [] in my example. By creating a collection class and pushing the logic to that we could handle pagination in that and support things like batching from the API etc in that object instead.

But for now I think it is ok to just stick it in the main method. Check this for more on why I think that: #149 (base gem work can be found at https://github.com/Accodeing/rest_easy)

def last_page?
Copy link
Member

Choose a reason for hiding this comment

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

No longer needed.

current_page == total_pages
end

def next_page
Copy link
Member

Choose a reason for hiding this comment

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

No longer needed.

current_page + 1
end

def current_page
Copy link
Member

Choose a reason for hiding this comment

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

No longer needed.

metadata&.current_page || 0
end

def total_pages
metadata&.total_pages || 0
end
end
end
end
end
15 changes: 15 additions & 0 deletions spec/fortnox/api/mappers/metadata_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

require 'spec_helper'
require 'fortnox/api'
require 'fortnox/api/mappers/metadata'
require 'fortnox/api/mappers/examples/mapper'

describe Fortnox::API::Mapper::Metadata do
key_map = Fortnox::API::Mapper::Metadata::KEY_MAP
json_entity_type = 'MetaInformation'

it_behaves_like 'mapper', key_map, json_entity_type, nil do
let(:mapper) { described_class.new }
end
end
19 changes: 19 additions & 0 deletions spec/fortnox/api/models/metadata_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'spec_helper'
require 'fortnox/api/models/metadata'
require 'fortnox/api/models/examples/model'

describe Fortnox::API::Model::Metadata, type: :model do
let(:required_attributes) do
{
current_page: 1,
total_resources: 2,
total_pages: 1
}
end

it 'can be initialized' do
expect { described_class.new(required_attributes) }.not_to raise_error
end
end
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/article_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
:ean,
'5901234123457'

include_examples '.all', 12
include_examples '.all', 20

include_examples '.find', '1' do
let(:find_by_hash_failure) { { description: 'Not Found' } }
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ class Test < Fortnox::API::Repository::Base

context 'when raising error from remote server' do
error_message = 'Räkenskapsår finns inte upplagt. '\
'För att kunna skapa en faktura krävs det att det finns ett räkenskapsår'
'För att kunna skapa en faktura krävs det att det finns ett räkenskapsår'

before do
Fortnox::API.configure do |conf|
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/customer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
# It is not yet possible to delete Customers. Therefore, expected nr of
# Customers when running .all will continue to increase
# (until 100, which is max by default).
include_examples '.all', 100
include_examples '.all', 209

include_examples '.find', '1' do
let(:find_by_hash_failure) { { city: 'Not Found' } }
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/invoice_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

# It is not possible to delete Invoces. Therefore, expected nr of Orders
# when running .all will continue to increase (until 100, which is max by default).
include_examples '.all', 60
include_examples '.all', 84

include_examples '.find', 1 do
let(:find_by_hash_failure) { { yourreference: 'Not found' } }
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/order_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

# It is not possible to delete Orders. Therefore, expected nr of Orders
# when running .all will continue to increase (until 100, which is max by default).
include_examples '.all', 100
include_examples '.all', 325

include_examples '.find', 1 do
let(:find_by_hash_failure) { { ourreference: 'Not found' } }
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/project_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
# It is not yet possible to delete Projects. Therefore, expected nr of
# Projects when running .all will continue to increase
# (until 100, which is max by default).
include_examples '.all', 8
include_examples '.all', 26

include_examples '.find', '1' do
let(:find_by_hash_failure) { { offset: 10_000 } }
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/terms_of_payment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

include_examples '.save', :description, additional_attrs: required_hash

include_examples '.all', 9
include_examples '.all', 10

include_examples '.find', '15DAYS', find_by_hash: false do
let(:find_by_hash_failure) { { code: '15days' } }
Expand Down
2 changes: 1 addition & 1 deletion spec/fortnox/api/repositories/unit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
:code,
'woooh'

include_examples '.all', 6
include_examples '.all', 8

include_examples '.find', 'blarg', find_by_hash: false do
let(:find_by_hash_failure) { { code: 'notfound' } }
Expand Down
24 changes: 16 additions & 8 deletions spec/vcr_cassettes/articles/all.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading