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 date and datetime conversions #56

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ report = Fulfil::Report.new(client: fulfil, report_name: 'account.tax.summary.ir
report.execute(start_date: Date.new(2020, 12, 1), end_date: Date.new(2020, 12, 31))
```

## Date Ranges
Fulfil expects an ISO8601 date when retrieving a date range. The GEM will take a `date` or `datetime` and parse it into the object Fulfil expects.

```
datetime = DateTime.new(2024, 1, 1, 12, 0, 0)
fulfil_model.search(domain: [['create_date', '>=', datetime]])
```

## Rate limits

Fulfil's API applies rate limits to the API requests that it receives. Every request is subject to throttling under the general limits. In addition, there are resource-based rate limits and throttles.
Expand Down
7 changes: 5 additions & 2 deletions lib/fulfil/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require 'http'
require 'logger'
require 'fulfil/response_parser'
require 'fulfil/domain_parser'

module Fulfil
SUBDOMAIN = ENV.fetch('FULFIL_SUBDOMAIN', nil)
Expand Down Expand Up @@ -69,15 +70,17 @@ def find_many(model:, ids:, fields: nil)

def search(model:, domain:, offset: nil, limit: 100, sort: nil, fields: %w[id])
uri = URI("#{model_url(model: model)}/search_read")
body = [domain, offset, limit, sort, fields]
parsed_domain = Fulfil::DomainParser.new(domain).parsed
body = [parsed_domain, offset, limit, sort, fields]

results = request(verb: :put, endpoint: uri, json: body)
parse(results: results)
end

def count(model:, domain:)
uri = URI("#{model_url(model: model)}/search_count")
body = [domain]
parsed_domain = Fulfil::DomainParser.new(domain).parsed
body = [parsed_domain]

request(verb: :put, endpoint: uri, json: body)
end
Expand Down
29 changes: 29 additions & 0 deletions lib/fulfil/converter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Fulfil
# The Fulfil::Conversions module provides utility methods for converting
# Date and DateTime objects into a standardized hash format.
class Converter
class << self
def date_or_datetime_as_object(date_or_datetime)
case date_or_datetime
when Date
date_as_object(date_or_datetime)
when DateTime
datetime_as_object(date_or_datetime)
end
end

def datetime_as_object(datetime)
{
__class__: 'datetime',
iso_string: datetime.new_offset(0).iso8601
}
end

def date_as_object(date)
datetime_as_object(date.to_datetime)
end
end
end
end
64 changes: 64 additions & 0 deletions lib/fulfil/domain_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

require 'fulfil/converter'

module Fulfil
# The Fulfil::DomainParser module provides utility methods for converting
# Date and DateTime objects into a standardized hash format.
# The module iterates over given parameters, identifies
# Date and DateTime objects, and converts them into a hash with a class descriptor
# and an ISO 8601 formatted string.
class DomainParser
attr_reader :domain

def initialize(domain)
@domain = domain

disable_escape_html_entities
end

def parsed
new_domain = domain.map do |values|
update_values(values)
end

enable_escape_html_entities
new_domain
end

def update_values(values)
values.map do |value|
case value.class.name
when 'Date'
date_as_object(value)
when 'DateTime'
datetime_as_object(value)
else
value
end
end
end

private

def date_as_object(date)
Copy link

Choose a reason for hiding this comment

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

Fulfil::DomainParser#date_as_object doesn't depend on instance state (maybe move it to another class?)

Converter.date_as_object(date)
end

def datetime_as_object(datetime)
Copy link

Choose a reason for hiding this comment

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

Fulfil::DomainParser#datetime_as_object doesn't depend on instance state (maybe move it to another class?)

Converter.datetime_as_object(datetime)
end

def disable_escape_html_entities
Copy link

Choose a reason for hiding this comment

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

Fulfil::DomainParser#disable_escape_html_entities doesn't depend on instance state (maybe move it to another class?)

return unless defined?(ActiveSupport) && ActiveSupport.respond_to?(:escape_html_entities_in_json=)
Copy link

Choose a reason for hiding this comment

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

Fulfil::DomainParser#disable_escape_html_entities manually dispatches method call

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cdmwebs rake test was erroring out when my guard clause was only return unless defined?(ActiveSupport) not sure exactly what to do to fix this codeclimate reek.


ActiveSupport.escape_html_entities_in_json = false
end

def enable_escape_html_entities
Copy link

Choose a reason for hiding this comment

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

Fulfil::DomainParser#enable_escape_html_entities doesn't depend on instance state (maybe move it to another class?)

return unless defined?(ActiveSupport) && ActiveSupport.respond_to?(:escape_html_entities_in_json=)
Copy link

Choose a reason for hiding this comment

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

Fulfil::DomainParser#enable_escape_html_entities manually dispatches method call


ActiveSupport.escape_html_entities_in_json = true
end
end
end
1 change: 1 addition & 0 deletions lib/fulfil/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def search(
offset: nil,
sort: nil
)

@client.search(
model: model,
domain: domain,
Expand Down
21 changes: 19 additions & 2 deletions lib/fulfil/query.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# frozen_string_literal: true

require 'date'
require 'fulfil/converter'

module Fulfil
class Query
def initialize
Expand Down Expand Up @@ -86,11 +89,25 @@ def build_search_term(field:, value:, options:, prefix: nil)
[[key, 'ilike', value]]
end
when 'Hash'
handle_hash(field, key, value, options)
when 'Date', 'DateTime'
[[key, '=', Converter.date_or_datetime_as_object(value)]]
else
raise "Unhandled value type: #{value} (#{value.class.name})"
end
end

def handle_hash(field, key, value, options)
Copy link

Choose a reason for hiding this comment

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

Fulfil::Query#handle_hash has 4 parameters

Copy link

Choose a reason for hiding this comment

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

Fulfil::Query#handle_hash has approx 7 statements

if %i[gte gt lte lt].any? { |op| value.key?(op) }
Copy link

Choose a reason for hiding this comment

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

Fulfil::Query#handle_hash refers to 'value' more than self (maybe move it to another class?)

value.map do |operator, val|
op_map = { gte: '>=', gt: '>', lte: '<=', lt: '<' }
converted_value = Converter.date_or_datetime_as_object(val)
[key, op_map[operator], converted_value]
end
else
value.flat_map do |nested_field, nested_value|
build_search_term(prefix: field, field: nested_field, value: nested_value, options: options)
end
else
raise "Unhandled value type: #{value} (#{value.class.name})"
end
end

Expand Down
77 changes: 77 additions & 0 deletions test/fulfil/client_test.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'test_helper'
require 'date'

module Fulfil
class ClientTest < Minitest::Test
Expand Down Expand Up @@ -68,5 +69,81 @@ def test_do_not_retry_when_retry_is_disabled
end
end
end

def test_date_conversion_in_search
date = Date.new(2022, 1, 1)
datetime = date.to_datetime
utc_datetime = datetime.new_offset(0)
expected_formatted_date = {
__class__: 'datetime',
iso_string: utc_datetime.iso8601
}

# Convert the expected hash's symbol keys to strings for comparison
expected_formatted_date_with_string_keys = expected_formatted_date.transform_keys(&:to_s)

stub_request(:put, fulfil_url_for('sale.sale/search_read'))
.to_return(status: 200, body: [].to_json, headers: { 'Content-Type' => 'application/json' })

client = Fulfil::Client.new
fulfil_model = Fulfil::Model.new(client: client, model_name: 'sale.sale')
fulfil_model.search(domain: [['create_date', '>=', date]])

assert_requested(:put, fulfil_url_for('sale.sale/search_read'), times: 1) do |request|
actual_formatted_date = JSON.parse(request.body).dig(0, 0, 2)

assert_equal expected_formatted_date_with_string_keys, actual_formatted_date
end
end

def test_datetime_conversion_in_search
datetime = DateTime.new(2022, 1, 1, 12, 0, 0)
utc_datetime = datetime.new_offset(0)
expected_formatted_date = {
__class__: 'datetime',
iso_string: utc_datetime.iso8601
}

# Convert the expected hash's symbol keys to strings for comparison
expected_formatted_date_with_string_keys = expected_formatted_date.transform_keys(&:to_s)

stub_request(:put, fulfil_url_for('sale.sale/search_read'))
.to_return(status: 200, body: [].to_json, headers: { 'Content-Type' => 'application/json' })

client = Fulfil::Client.new
fulfil_model = Fulfil::Model.new(client: client, model_name: 'sale.sale')
fulfil_model.search(domain: [['create_date', '>=', datetime]])

assert_requested(:put, fulfil_url_for('sale.sale/search_read'), times: 1) do |request|
actual_formatted_date = JSON.parse(request.body).dig(0, 0, 2)

assert_equal expected_formatted_date_with_string_keys, actual_formatted_date
end
end

def test_datetime_conversion_in_count
datetime = DateTime.new(2022, 1, 1, 12, 0, 0)
utc_datetime = datetime.new_offset(0)
expected_formatted_date = {
__class__: 'datetime',
iso_string: utc_datetime.iso8601
}

# Convert the expected hash's symbol keys to strings for comparison
expected_formatted_date_with_string_keys = expected_formatted_date.transform_keys(&:to_s)

stub_request(:put, fulfil_url_for('sale.sale/search_count'))
.to_return(status: 200, body: [].to_json, headers: { 'Content-Type' => 'application/json' })

client = Fulfil::Client.new
fulfil_model = Fulfil::Model.new(client: client, model_name: 'sale.sale')
fulfil_model.count(domain: [['create_date', '>=', datetime]])

assert_requested(:put, fulfil_url_for('sale.sale/search_count'), times: 1) do |request|
actual_formatted_date = JSON.parse(request.body).dig(0, 0, 2)

assert_equal expected_formatted_date_with_string_keys, actual_formatted_date
end
end
end
end
30 changes: 30 additions & 0 deletions test/fulfil/query_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

require 'minitest/autorun'
require 'fulfil/query'
require 'fulfil/converter'
require 'date'

# rubocop:disable Metrics/ClassLength
module Fulfil
class QueryTest < Minitest::Test
def setup
Expand Down Expand Up @@ -47,6 +50,32 @@ def test_nested_queries
assert_equal [['sale.id', 'in', [123]]], @query.query
end

def test_dates_are_converted_to_iso8601
date = Date.new(2019, 1, 1)
expected_date_format = Converter.date_as_object(date)

@query.search(date: date)

assert_equal [['date', '=', expected_date_format]], @query.query
end

def test_date_greater_than_or_equal_to_converted_to_iso8601
date = Date.new(2019, 1, 1)
expected_date_format = Converter.date_as_object(date)

@query.search(date: { gte: date })

assert_equal [['date', '>=', expected_date_format]], @query.query
end

def test_datetime_less_than_converted_to_iso8601
datetime = DateTime.new(2019, 1, 1, 0, 0, 0)
expected_datetime_format = Converter.datetime_as_object(datetime)

@query.search(datetime: { lt: datetime })

assert_equal [['datetime', '<', expected_datetime_format]], @query.query
end
# -- #exclude -----------------------

def test_equals_exclude
Expand Down Expand Up @@ -183,3 +212,4 @@ def test_chaining
end
end
end
# rubocop:enable Metrics/ClassLength
Loading