pipe_line_dealer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/.gitignore +3 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +3 -0
  5. data/Guardfile +5 -0
  6. data/LICENCE +7 -0
  7. data/README.md +19 -0
  8. data/api_remarks.txt +5 -0
  9. data/lib/pipe_line_dealer/client/connection.rb +71 -0
  10. data/lib/pipe_line_dealer/client.rb +27 -0
  11. data/lib/pipe_line_dealer/collection/concerns/create_and_update.rb +25 -0
  12. data/lib/pipe_line_dealer/collection/concerns/fetching.rb +64 -0
  13. data/lib/pipe_line_dealer/collection/concerns/results_fetcher.rb +64 -0
  14. data/lib/pipe_line_dealer/collection/concerns.rb +6 -0
  15. data/lib/pipe_line_dealer/collection.rb +24 -0
  16. data/lib/pipe_line_dealer/custom_field/collection.rb +18 -0
  17. data/lib/pipe_line_dealer/custom_fields.rb +59 -0
  18. data/lib/pipe_line_dealer/error/connection.rb +17 -0
  19. data/lib/pipe_line_dealer/error/invalid_attribute.rb +29 -0
  20. data/lib/pipe_line_dealer/error/no_such_custom_field.rb +12 -0
  21. data/lib/pipe_line_dealer/error.rb +9 -0
  22. data/lib/pipe_line_dealer/limits.rb +7 -0
  23. data/lib/pipe_line_dealer/model/base/concern/persistance.rb +69 -0
  24. data/lib/pipe_line_dealer/model/base.rb +133 -0
  25. data/lib/pipe_line_dealer/model/company/custom_field.rb +7 -0
  26. data/lib/pipe_line_dealer/model/company.rb +37 -0
  27. data/lib/pipe_line_dealer/model/custom_field.rb +73 -0
  28. data/lib/pipe_line_dealer/model/note.rb +32 -0
  29. data/lib/pipe_line_dealer/model/person/custom_field.rb +7 -0
  30. data/lib/pipe_line_dealer/model/person.rb +36 -0
  31. data/lib/pipe_line_dealer/version.rb +10 -0
  32. data/lib/pipe_line_dealer.rb +44 -0
  33. data/pipe_line_dealer.gemspec +45 -0
  34. data/spec/acceptance/companies/creation_spec.rb +31 -0
  35. data/spec/acceptance/companies/custom_fields_spec.rb +7 -0
  36. data/spec/acceptance/companies/updating_spec.rb +35 -0
  37. data/spec/acceptance/people/creation_spec.rb +19 -0
  38. data/spec/collection_spec.rb +166 -0
  39. data/spec/custom_fields_spec.rb +114 -0
  40. data/spec/helper.rb +32 -0
  41. data/spec/models/base/concern/persistance_spec.rb +55 -0
  42. data/spec/models/base_spec.rb +32 -0
  43. data/spec/models/custom_field_spec.rb +127 -0
  44. data/spec/smell_spec.rb +6 -0
  45. data/spec/support/acceptance_helper.rb +39 -0
  46. data/spec/support/collection_helper.rb +8 -0
  47. data/spec/support/connection_helper.rb +9 -0
  48. data/spec/support/pld_cleaner.rb +16 -0
  49. data/spec/support/test_model.rb +6 -0
  50. data/todo.txt +6 -0
  51. metadata +320 -0
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ coverage/
3
+ spec/api.key
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format Fuubar
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ script: bundle exec rspec
6
+ notifications:
7
+ email:
8
+ - maarten+pld_travis@moretea.nl
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard "rspec" do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch("spec/helper.rb") { "spec" }
5
+ end
data/LICENCE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 Maarten Hoogendoorn (maarten@springest.com)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,19 @@
1
+ #Pipe line dealer
2
+
3
+ [<img src="https://secure.travis-ci.org/moretea/pipe_line_dealer.png?travis"/>](http://travis-ci.org/moretea/pipe_line_dealer) [<img src="https://gemnasium.com/moretea/pipe_line_dealer.png?travis"/>](https://gemnasium.com/moretea/pipe_line_dealer)
4
+
5
+ Pipe line dealer is client for the [pipe line deals api](http://www.pipelinedeals.com/developers/api).
6
+
7
+ This gem is young. If you want to know anything, just send me a message.
8
+
9
+ # Acceptance specs
10
+ In order to run the acceptance tests agains the actual API, you'll need to provide an API key,
11
+ and set the ```ACCEPTANCE=true``` environmental variable.
12
+ This can be done either by providing it as another environmental variable ```API_KEY=secret```,
13
+ or by adding a file in ```spec/api.key``` with the API key.
14
+
15
+ # Thanks
16
+ Many thanks to my employer [<img src="http://static-1.cdnhub.nl/images/logo-springest.jpg" alt="Springest">](http://springest.com/).
17
+
18
+
19
+ They are awesome :) If you're looking for a [job (In the Netherlands)](http://www.springest.nl/weblog/vacature-ruby-on-rails-developer-in-amsterdam), check it out.
data/api_remarks.txt ADDED
@@ -0,0 +1,5 @@
1
+ Improvements for pipelinedeals.com:
2
+ - DELETE returns 200, and empty string. Could it return a valid JSON response?
3
+ - If you try to save an invalid hash, we get a 500. Could this be changed to
4
+ unprocessable entity, and have the same error format as the other requests?
5
+ - Make it possible to manage custom fields via the API.
@@ -0,0 +1,71 @@
1
+ module PipeLineDealer
2
+ class Client
3
+ class Connection
4
+ attr_accessor :cache
5
+
6
+ DEFAULT_OPTIONS = {
7
+ http_debug: false
8
+ }
9
+
10
+ def initialize(key, options)
11
+ @key = key
12
+ @cache = {}
13
+ @options = DEFAULT_OPTIONS.merge options
14
+ @connection = build_connection
15
+ end
16
+
17
+ def build_connection
18
+ Faraday.new(url: ENDPOINT) do |faraday|
19
+ faraday.request :url_encoded
20
+ faraday.response :logger if @options[:http_debug]
21
+ faraday.adapter Faraday.default_adapter
22
+ end
23
+ end
24
+
25
+ def cache(key, &block)
26
+ if not @cache.has_key?(key)
27
+ @cache[key] = block.call
28
+ end
29
+
30
+ @cache[key]
31
+ end
32
+
33
+ # HTTP methods
34
+ [:get, :post, :put, :delete].each do |method|
35
+ class_eval <<-RUBY
36
+ def #{method} url, params = {}
37
+ request(:#{method}, url, params)
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ protected
43
+
44
+ def request(method, request_url, params)
45
+ begin
46
+ case method
47
+ when :get, :delete
48
+ request_params = params.merge(api_key: @key)
49
+ query = Faraday::Utils.build_nested_query(request_params)
50
+ response = @connection.send(method, request_url + "?" + query)
51
+ when :post, :put
52
+ request_url += "?api_key=#{@key}"
53
+ body = Faraday::Utils.build_nested_query(params)
54
+ response = @connection.send(method, request_url, body)
55
+ end
56
+
57
+ if method == :delete
58
+ # Bwelch. Only a delete doesn't return valid json :(
59
+ return [response.status, JSON.parse('{ "msg": "' + response.body + '" }')]
60
+ else
61
+ return [response.status, JSON.parse(response.body)]
62
+ end
63
+ end
64
+ end
65
+
66
+ def debug?
67
+ ENV['DEBUG'] == "true"
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,27 @@
1
+ module PipeLineDealer
2
+ ENDPOINT = "https://api.pipelinedeals.com/api/v3/"
3
+
4
+ class Client
5
+ attr_reader :connection
6
+
7
+ def initialize(api_key, options = {})
8
+ @connection = Connection.new(api_key, options)
9
+ end
10
+
11
+ def companies
12
+ Collection.new(self, klass: Company)
13
+ end
14
+
15
+ def custom_company_fields
16
+ CustomField::Collection.new(self, klass: Company::CustomField)
17
+ end
18
+
19
+ def people
20
+ Collection.new(self, klass: Person)
21
+ end
22
+
23
+ def custom_person_fields
24
+ CustomField::Collection.new(self, klass: Person::CustomField)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module PipeLineDealer
2
+ class Collection
3
+ module Concerns
4
+ module CreateAndUpdate
5
+ extend ActiveSupport::Concern
6
+
7
+ def create(attributes = {})
8
+ if @options[:new_defaults]
9
+ attributes = @options[:new_defaults].merge(attributes)
10
+ end
11
+
12
+ @klass.new(collection: self, record_new: true, attributes: attributes).save
13
+ end
14
+
15
+ def new(attributes = {})
16
+ if @options[:new_defaults]
17
+ attributes = @options[:new_defaults].merge(attributes)
18
+ end
19
+
20
+ @klass.new(collection: self, record_new: true, attributes: attributes)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ module PipeLineDealer
2
+ class Collection
3
+ module Concerns
4
+ module Fetching
5
+ extend ActiveSupport::Concern
6
+
7
+ def where(opts)
8
+ new_options = @options.merge(where: opts)
9
+ self.class.new(@client, new_options)
10
+ end
11
+
12
+ def on_new_defaults(opts)
13
+ new_options = @options.merge(new_defaults: opts)
14
+ self.class.new(@client, new_options)
15
+ end
16
+
17
+ def all
18
+ if @options[:cached]
19
+ @client.connection.cache(@options[:cache_key]) { self.to_a }
20
+ else
21
+ self
22
+ end
23
+ end
24
+
25
+ def limit(lmt)
26
+ refine(limit: lmt)
27
+ end
28
+
29
+ def first
30
+ self.limit(1).to_a.first
31
+ end
32
+
33
+ def to_a
34
+ array = []
35
+ self.each { |item| array << item }
36
+
37
+ array
38
+ end
39
+
40
+ def each &block
41
+ options = @options.dup
42
+ ResultsFetcher.new(self, options).each &block
43
+ end
44
+
45
+ #TODO: Not speced
46
+ def find id
47
+ status, result = @client.connection.get(collection_url + "/#{id}.json", {})
48
+ if status == 200
49
+ @klass.new(collection: self, persisted: true, attributes: result)
50
+ else
51
+ raise Error::NotFound
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ # Refine the criterea
58
+ def refine(refinements)
59
+ self.class.new(@client, @options.merge(refinements))
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ module PipeLineDealer
2
+ class Collection
3
+ module Concerns
4
+ class ResultsFetcher
5
+ attr_reader :options
6
+
7
+ def initialize(collection, options)
8
+ @collection = collection
9
+ @options = options
10
+ end
11
+
12
+ def limit
13
+ options[:limit] || PipeLineDealer::Limits::MAX_RESULTS_PER_PAGE
14
+ end
15
+
16
+ def each &block
17
+ @params = { page: 1, per_page: PipeLineDealer::Limits::MAX_RESULTS_PER_PAGE }
18
+
19
+ @results_yielded = 0
20
+
21
+ # Enforce maximum number of resulst per page.
22
+ if limit && limit > PipeLineDealer::Limits::MAX_RESULTS_PER_PAGE
23
+ options[:limit] = PipeLineDealer::Limits::MAX_RESULTS_PER_PAGE
24
+ end
25
+
26
+ @params[:per_page] = limit
27
+
28
+ #TODO: refactor this deal specific piece of code.
29
+ if options.has_key?(:where) && options[:where].has_key?(:query) && options[:where][:query].has_key?(:company_id)
30
+ @params[:company_id] = options[:where][:query][:company_id].to_s
31
+ end
32
+
33
+ catch :stop_yielding do
34
+ while true
35
+ status, result = @collection.client.connection.get(@collection.collection_url + ".json", @params)
36
+
37
+ if status == 200
38
+ yield_results(result, &block)
39
+ else # status != 200
40
+ #TODO
41
+ raise "Unexpected status #{status.inspect}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def yield_results result, &block
48
+ result["entries"].each do |entry|
49
+ block.call(@collection.klass.new(collection: @collection, persisted: true, attributes: entry))
50
+ @results_yielded += 1
51
+
52
+ throw :stop_yielding if @results_yielded >= limit
53
+ end
54
+
55
+ if result["pagination"]["page"] < result["pagination"]["pages"]
56
+ @params[:page] += 1
57
+ else
58
+ throw :stop_yielding
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,6 @@
1
+ module PipeLineDealer
2
+ class Collection
3
+ module Concerns
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,24 @@
1
+ module PipeLineDealer
2
+ class Collection
3
+ include Concerns::Fetching
4
+ include Concerns::CreateAndUpdate
5
+
6
+ DEFAULT_OPTIONS = { cached: false }
7
+
8
+ attr_reader :client
9
+ attr_reader :klass
10
+ def initialize(client, options = {})
11
+ @options = DEFAULT_OPTIONS.merge(options)
12
+ @client = client
13
+ @klass = options[:klass]
14
+ end
15
+
16
+ def to_s
17
+ "<PipeLineDealer::Collection:#{__id__} @klass=#{@klass.inspect}>"
18
+ end
19
+
20
+ def collection_url
21
+ @klass.instance_variable_get(:@collection_url)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ module PipeLineDealer
2
+ class CustomField
3
+ class Collection < PipeLineDealer::Collection
4
+ def initialize(client, options={})
5
+ options[:cached] = true
6
+ super(client, options)
7
+ end
8
+
9
+ def [](key)
10
+ self.all.each do |item|
11
+ return item if item.name== key
12
+ end
13
+
14
+ raise PipeLineDealer::Error::NoSuchCustomField.new(key)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,59 @@
1
+ # "Plugin"
2
+
3
+ module PipeLineDealer
4
+ module CustomFields
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attr_reader :custom_fields
9
+ end
10
+
11
+ CUSTOM_FIELD_PREFIX = "custom_label_"
12
+ CUSTOM_FIELD_REGEX = /#{CUSTOM_FIELD_PREFIX}(\d+)/
13
+
14
+ module InstanceMethods
15
+ protected
16
+
17
+ def custom_field_collection
18
+ method = self.class.instance_variable_get(:@custom_field_client_method)
19
+ @collection.client.send(method).all
20
+ end
21
+
22
+ def process_attributes
23
+ @custom_fields = pld2human_fields(@attributes.delete(:custom_fields))
24
+ end
25
+
26
+ def attributes_for_saving attributes
27
+ attributes.merge("custom_fields" => human2pld_fields(@custom_fields))
28
+ end
29
+
30
+ def pld2human_fields attributes
31
+ attributes ||= {}
32
+ result = {}
33
+
34
+ attributes.each do |key, value|
35
+ field_id = CUSTOM_FIELD_REGEX.match(key)[1].to_i
36
+ field = custom_field_collection.select { |field| field.id == field_id }.first
37
+
38
+ raise Error::InvalidAttributeName.new(self, key) if field.nil?
39
+ result[field.name] = field.decode(self, value)
40
+ end
41
+
42
+ result
43
+ end
44
+
45
+ def human2pld_fields attributes
46
+ attributes ||= {}
47
+ result = HashWithIndifferentAccess.new
48
+
49
+ attributes.each do |name, value|
50
+ field = custom_field_collection.select { |field| field.name == name}.first
51
+ raise Error::InvalidAttributeName.new(self, name) if field.nil?
52
+ result[CUSTOM_FIELD_PREFIX + field.id.to_s] = field.encode(self, value)
53
+ end
54
+
55
+ result
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,17 @@
1
+ module PipeLineDealer
2
+ class Error
3
+ class Connection < PipeLineDealer::Error
4
+ end
5
+
6
+ class Connection
7
+ class Unprocessable < Error::Connection
8
+ attr_reader :errors
9
+
10
+ def initialize(errors)
11
+ @errors = errors
12
+ @message = "Couldn't save to the server: #{errors.inspect}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ module PipeLineDealer
2
+ class Error
3
+ class InvalidAttribute < PipeLineDealer::Error
4
+ attr_reader :attribute_name
5
+
6
+ def initialize(klass, attribute_name)
7
+ @klass = klass
8
+ @attribute_name = attribute_name
9
+ end
10
+ end
11
+
12
+ class InvalidAttributeName < InvalidAttribute
13
+ def initialize(klass, attribute_name)
14
+ super
15
+ @message = "The attribute #{attribute_name.inspect} is not known by #{klass.inspect}!"
16
+ end
17
+ end
18
+
19
+ class InvalidAttributeValue < InvalidAttribute
20
+ attr_reader :value
21
+
22
+ def initialize(klass, attribute_name, value)
23
+ super(klass, attribute_name)
24
+ @value = value
25
+ @message = "The attribute #{attribute_name.inspect} does not accept the value #{value.inspect} (on model #{klass.inspect})"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ module PipeLineDealer
2
+ class Error
3
+ class NoSuchCustomField < PipeLineDealer::Error
4
+ attr_reader :name
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @message = "Could not find a custom field with the name #{name.inspect}"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module PipeLineDealer
2
+ class Error < Exception
3
+ attr_reader :message
4
+
5
+ def to_s
6
+ @message
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,7 @@
1
+ module PipeLineDealer
2
+ # There are the limits of the API
3
+ class Limits
4
+ MAX_RESULTS_PER_PAGE = 200
5
+ MAX_REQUESTS_PER_SECOND = 1
6
+ end
7
+ end
@@ -0,0 +1,69 @@
1
+ module PipeLineDealer
2
+ module Model
3
+ class Base
4
+ module Concern
5
+ module Persistance
6
+ include ActiveSupport::Concern
7
+
8
+ def new_record?
9
+ not @persisted
10
+ end
11
+
12
+ def persisted?
13
+ @persisted
14
+ end
15
+
16
+ IGNORE_ATTRIBUTES_WHEN_SAVING = [:updated_at, :created_at]
17
+
18
+ def save
19
+ # Use :post for new records, :put for existing ones
20
+ method = new_record? ? :post : :put
21
+
22
+ # Ignore some attributes
23
+ save_attrs = @attributes.reject { |k, v| IGNORE_ATTRIBUTES_WHEN_SAVING.member?(k) }
24
+
25
+ # And allow a model class to modify / edit attributes even further
26
+ if respond_to?(:attributes_for_saving)
27
+ save_attrs = attributes_for_saving(save_attrs)
28
+ end
29
+
30
+ # Execute the request
31
+ status, response = @collection.client.connection.send(method, object_url, model_attribute_name => save_attrs)
32
+
33
+ if status == 200
34
+ import_attributes!(response) # Set the stuff.
35
+ self
36
+ elsif status == 422
37
+ errors = response.collect { |error| { field: error["field"], message: error["msg"] } }
38
+ raise Error::Connection::Unprocessable.new(errors)
39
+ else
40
+ raise "Unexepcted response status #{status.inspect}"
41
+ end
42
+ end
43
+
44
+ def destroy
45
+ status, response = @collection.client.connection.send(:delete, object_url)
46
+ #TODO: Check response
47
+
48
+ if status == 200 && response["msg"] == " "
49
+ @destroyed = true #todo: store that this model has been destroyed.
50
+ true
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ def object_url
59
+ if persisted?
60
+ @collection.send(:collection_url) + "/" + self.id.to_s + ".json"
61
+ else
62
+ @collection.send(:collection_url) + ".json"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end