Skip to content

Commit

Permalink
Added Advanced Filters
Browse files Browse the repository at this point in the history
Added in the ability to use the advanced filters API. Specs mostly complete
  • Loading branch information
joynerd committed Oct 19, 2022
1 parent 75dfa97 commit 33c8bcd
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 17 deletions.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ gem "rspec", "~> 3.0"
gem "rubocop", "~> 1.21"

gem "faraday"

gem "webmock"

gem "sinatra"
23 changes: 23 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,28 @@ PATH
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
ast (2.4.2)
crack (0.4.5)
rexml
diff-lcs (1.5.0)
faraday (2.6.0)
faraday-net_http (>= 2.0, < 3.1)
ruby2_keywords (>= 0.0.4)
faraday-net_http (3.0.1)
hashdiff (1.0.1)
json (2.6.2)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
ostruct (0.5.5)
parallel (1.22.1)
parser (3.1.2.1)
ast (~> 2.4.1)
public_suffix (5.0.0)
rack (2.2.4)
rack-protection (3.0.2)
rack
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.6.0)
Expand Down Expand Up @@ -50,7 +61,17 @@ GEM
parser (>= 3.1.1.0)
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5)
sinatra (3.0.2)
mustermann (~> 3.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (= 3.0.2)
tilt (~> 2.0)
tilt (2.0.11)
unicode-display_width (2.3.0)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)

PLATFORMS
arm64-darwin-21
Expand All @@ -61,6 +82,8 @@ DEPENDENCIES
rake (~> 13.0)
rspec (~> 3.0)
rubocop (~> 1.21)
sinatra
webmock

BUNDLED WITH
2.3.7
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ lead.save

## Supported Resources

### Advanced Filters
[Close API Docs](https://developer.close.com/resources/leads/)

Advanced Filters are a clever way to open up search to the API, with the caveat that they are very dense and appear to be written in search DSL (Elastic, Solr, etc).

I have tried to encapsulate the complexity of the DSL into a simple interface that is easy to use. This is mostly by defining queries before
they are run and then running them with parameters when they are needed.

An example of an advanced filter query:

```ruby
# Run a prebuilt query
Close::AdvancedFilter.run('find_leads_by_email', {email: 'buster.bluth@gmail.com'})


```

### Leads
[Close API Docs](https://developer.close.com/resources/leads/)

Expand Down
Binary file added close-0.1.0.gem
Binary file not shown.
1 change: 1 addition & 0 deletions lib/close.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
require_relative "close/close_object"
require_relative "close/api_operations"
require_relative "close/api_resource"
require_relative "close/filter"
require_relative "close/resources"
require_relative "close/errors"
require_relative "close/version"
Expand Down
60 changes: 60 additions & 0 deletions lib/close/data/filters/find_lead_by_contact_email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"limit": null,
"query": {
"negate": false,
"queries": [
{
"negate": false,
"object_type": "lead",
"type": "object_type"
},
{
"negate": false,
"queries": [
{
"negate": false,
"related_object_type": "contact",
"related_query": {
"negate": false,
"queries": [
{
"negate": false,
"related_object_type": "contact_email",
"related_query": {
"negate": false,
"queries": [
{
"condition": {
"mode": "full_words",
"type": "text",
"value": "%EMAIL%"
},
"field": {
"field_name": "email",
"object_type": "contact_email",
"type": "regular_field"
},
"negate": false,
"type": "field_condition"
}
],
"type": "and"
},
"this_object_type": "contact",
"type": "has_related"
}
],
"type": "and"
},
"this_object_type": "lead",
"type": "has_related"
}
],
"type": "and"
}
],
"type": "and"
},
"results_limit": null,
"sort": []
}
2 changes: 2 additions & 0 deletions lib/close/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ class RateLimitExceeded < StandardError; end
class InvalidRequestError < StandardError; end
class AuthenticationError < StandardError; end
class APIError < StandardError; end
class QueryNotFoundError < StandardError; end
class MissingParameterError < StandardError; end
end
99 changes: 99 additions & 0 deletions lib/close/filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# This class attempts to abstract away the Advanced Filter API.
# It is very powerful and fast, but building the queries is very tedious.
# It allows to store preset queries as JSON for common queries that
# can be commited to the repo and validated. It also lets you
# define queries on the fly which can then be reused across the
# codebase without having to copy and paste the query all the time.
module Close
class Filter
extend APIOperations

@@queries = {}

# Executes a raw query against the Close API.
# @param [Hash] query The query to execute.
# @return [Array] An array of results.
def self.execute(query = {})
response = request(:post, 'api/v1/data/search/', query)
response['data']
end

# Executes a query by name.
# @param [String] name The name of the query to execute.
# @param [Hash] params The parameters to pass to the query.
# @return [Array] An array of results.
def self.run(name, params = {})
query_string = load_query_from_file(name)
expected_params = find_params(query_string)
preflight_params(params, expected_params)
parameterized_query = apply_params(query_string, params)
execute(parameterized_query)
end

# Loads a query from a file or from memory.
# @param [String] name The name of the query.
# @return [String] A stringified JSON query.
def self.load_query(name)
if @@queries[name.to_s]
@@queries[name.to_s]
else
load_query_from_file(name)
end
end

# This method is used to defined a query at run time.
# If a name collision occurs, the query will be overwritten.
# @param [String] name The name of the query.
# @param [Hash] query_body A hash with placeholders in keys.
# @return [Void]
def self.add_query(name, query_body)
@@queries[name.to_s] = query_body.to_json
end

# Applies the params to the query string.
# @param [String] query_string The stringified JSON query.
# @param [Hash] params The parameters to apply.
# @return [String] The stringified JSON query with the parameters applied.
def self.apply_params(query_string, params)
qs = query_string.dup
params.each do |key, value|
qs.gsub!(/%#{key.upcase}%/, value)
end
qs
end

# Check that all of the params are present in expected_params.
# @param [Hash] params The parameters to check.
# @param [Array] expected_params The expected parameters.
# @return [Void]
# @raise [Close::MissingParameterError] if a parameter is missing.
def self.preflight_params(params, expected_params)
expected_params.each do |param|
if !params.transform_keys(&:to_s).has_key?(param.to_s)
raise Close::MissingParameterError, "Missing parameter: #{param}"
end
end
end

# Loads a predefined query from a file.
# @param [String] name The name of the query.
# @return [String] A stringified JSON query.
# @raise [Close::QueryNotFoundError] if the file does not exist.
def self.load_query_from_file(name)
begin
file = File.read("lib/close/data/filters/#{name}.json")
rescue Errno::ENOENT
raise Close::QueryNotFoundError.new("Query #{name} not found.")
end
end

# Scans a string a returns the parameters it will expect
# when executed.
# @param [String] str The string to scan.
# @return [Array] An array of parameters.
def self.find_params(str)
str.scan(/%[A-Z]+(?:_[A-Z]+)*%/).map{ |x| x[1..-2].downcase }.uniq
end

end
end
2 changes: 1 addition & 1 deletion lib/close/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module Close
VERSION = "0.1.0"
VERSION = "0.1.1"
end
10 changes: 5 additions & 5 deletions spec/close/api_resource_spec.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
# frozen_string_literal: true

RSpec.describe Close::ApiResource do
RSpec.describe Close::APIResource do

describe ".resource_url" do
it "Raises a not implmented error" do
expect{Close::ApiResource.resource_url}.to raise_error(NotImplementedError)
expect{Close::APIResource.resource_url}.to raise_error(NotImplementedError)
end
end

describe "#dirty?" do
context "when the resource is not dirty" do
it "returns false" do
expect(Close::ApiResource.new.dirty?).to eq(false)
expect(Close::APIResource.new.dirty?).to eq(false)
end
end
context "when the resource has unsaved values" do
it "returns true" do
expect(Close::ApiResource.new(name: "Test").dirty?).to eq(true)
expect(Close::APIResource.new(name: "Test").dirty?).to eq(true)
end
end
context "when the resource has unsaved values and is saved" do
it "returns false" do
resource = Close::ApiResource.new(name: "Test")
resource = Close::APIResource.new(name: "Test")
resource.save
expect(resource.dirty?).to eq(false)
end
Expand Down
8 changes: 0 additions & 8 deletions spec/close/client_spec.rb

This file was deleted.

Loading

0 comments on commit 33c8bcd

Please sign in to comment.