rescue_groups 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +90 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +226 -0
  8. data/Rakefile +30 -0
  9. data/config/config.rb +26 -0
  10. data/config/initializer.rb +15 -0
  11. data/docs/animal_field.md +138 -0
  12. data/docs/event_field.md +20 -0
  13. data/docs/organization_field.md +25 -0
  14. data/fields/animal_field.rb +152 -0
  15. data/fields/event_field.rb +35 -0
  16. data/fields/organization_field.rb +40 -0
  17. data/fields/picture_field.rb +30 -0
  18. data/lib/api_client.rb +29 -0
  19. data/lib/queryable.rb +79 -0
  20. data/lib/relationable.rb +76 -0
  21. data/lib/remote_client.rb +29 -0
  22. data/lib/remote_model.rb +47 -0
  23. data/lib/requests/find.rb +29 -0
  24. data/lib/requests/invalid_client.rb +1 -0
  25. data/lib/requests/where.rb +94 -0
  26. data/lib/response.rb +48 -0
  27. data/models/animal.rb +57 -0
  28. data/models/event.rb +41 -0
  29. data/models/organization.rb +41 -0
  30. data/models/picture.rb +26 -0
  31. data/rescue_groups.gemspec +28 -0
  32. data/rescue_groups.rb +27 -0
  33. data/search/animal_search.rb +15 -0
  34. data/search/base_search.rb +72 -0
  35. data/search/event_search.rb +15 -0
  36. data/search/filter.rb +49 -0
  37. data/search/organization_search.rb +15 -0
  38. data/spec/fixtures/animal/find.json +1 -0
  39. data/spec/fixtures/animal/where.json +1 -0
  40. data/spec/fixtures/error.json +20 -0
  41. data/spec/fixtures/event/find.json +1 -0
  42. data/spec/fixtures/event/where.json +1 -0
  43. data/spec/fixtures/organization/find.json +1 -0
  44. data/spec/fixtures/organization/where.json +1 -0
  45. data/spec/fixtures/test_constants.rb +12 -0
  46. data/spec/integration/animal_spec.rb +55 -0
  47. data/spec/integration/event_spec.rb +33 -0
  48. data/spec/integration/organization_spec.rb +35 -0
  49. data/spec/lib/queryable_spec.rb +257 -0
  50. data/spec/lib/relationable_spec.rb +113 -0
  51. data/spec/lib/remote_client_spec.rb +27 -0
  52. data/spec/lib/requests/find_spec.rb +97 -0
  53. data/spec/lib/requests/where_spec.rb +267 -0
  54. data/spec/lib/response_spec.rb +99 -0
  55. data/spec/models/animal_spec.rb +131 -0
  56. data/spec/models/event_spec.rb +105 -0
  57. data/spec/models/organization_spec.rb +112 -0
  58. data/spec/models/picture_spec.rb +87 -0
  59. data/spec/search/animal_search_spec.rb +8 -0
  60. data/spec/search/event_search_spec.rb +8 -0
  61. data/spec/search/filter_spec.rb +39 -0
  62. data/spec/search/organization_search_spec.rb +8 -0
  63. data/spec/spec_helper.rb +340 -0
  64. data/spec/support/model_spec.rb +47 -0
  65. data/spec/support/searchable_spec.rb +15 -0
  66. data/support/animal_mock.rb +215 -0
  67. data/support/base_mock.rb +44 -0
  68. data/support/event_mock.rb +48 -0
  69. data/support/organization_mock.rb +53 -0
  70. data/version.rb +3 -0
  71. metadata +242 -0
@@ -0,0 +1,29 @@
1
+ require 'httparty'
2
+ require_relative './response'
3
+
4
+ module RescueGroups
5
+ # HTTParty wrapper with defaulted values
6
+ # for Content-Type, URL and apikey authentication
7
+ class RemoteClient
8
+ include HTTParty
9
+
10
+ base_uri 'https://api.rescuegroups.org/http/json'
11
+ headers 'Content-Type' => 'application/json'
12
+
13
+ # method: post_and_respond
14
+ # purpose: make a POST request to the RescueGroups API and respond
15
+ # param: post_body - <Hash> - attributes to be included in the post body
16
+ # return: response <Response> object with details of HTTP response
17
+ def post_and_respond(post_body)
18
+ if RescueGroups.config.apikey.nil? || RescueGroups.config.apikey.length == 0
19
+ raise 'No RescueGroups API Key set'
20
+ end
21
+
22
+ response = self.class.post(
23
+ self.class.base_uri,
24
+ { body: JSON(post_body.merge(apikey: RescueGroups.config.apikey)) })
25
+
26
+ Response.new(response)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ require_relative './api_client'
2
+ require_relative '../lib/queryable'
3
+ require_relative '../lib/relationable'
4
+ require_relative '../models/picture'
5
+
6
+ module RescueGroups
7
+ class RemoteModel
8
+ include ApiClient
9
+ include Queryable
10
+ include Relationable
11
+
12
+ # method: initialize
13
+ # purpose: given a hash of attributes, assign the attributes that the
14
+ # included model has defined and discard the rest
15
+ # param: attribute_hash - <Hash> - hash of attributes to instantiate
16
+ # this model with
17
+ # return: nil
18
+ def initialize(attribute_hash = {})
19
+ (attribute_hash || {}).each do |key, value|
20
+ mapped_key = self.class.object_fields::FIELDS.key(key.to_sym)
21
+ mapped_method = "#{ mapped_key }="
22
+ next unless self.respond_to?(mapped_method)
23
+ self.send(mapped_method, cast_attribute_to_type(mapped_key, value))
24
+ end
25
+ end
26
+
27
+ def cast_attribute_to_type(name, value)
28
+ return value.to_i if name.to_s == 'id' || name =~ /_id/
29
+
30
+ value
31
+ end
32
+
33
+ # method: attributes
34
+ # purpose: Distill the included class's attributes into a hash of keys and values
35
+ # If an attribute is nil, the key for that attribute is still included
36
+ # in the resulting hash and the value is nil
37
+ # param: none
38
+ # return: <Hash> of attributes from the included class
39
+ def attributes
40
+ {}.tap do |hash|
41
+ self.class.object_fields::FIELDS.keys.each do |attribute|
42
+ hash[attribute] = instance_variable_get(:"@#{ attribute }")
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ require_relative './invalid_client'
2
+
3
+ module RescueGroups
4
+ module Requests
5
+ class Find
6
+ def initialize(ids, model, client)
7
+ @ids = [*ids].flatten.uniq.map(&:to_i)
8
+ @model = model
9
+ @client = client
10
+ rescue NoMethodError
11
+ raise 'Only initialize a Requests::Find with integers or models responding to .to_i'
12
+ end
13
+
14
+ def request
15
+ raise InvalidClient, 'Invalid client given to Requests::Find' unless @client.respond_to?(:post_and_respond)
16
+ @client.post_and_respond(as_json)
17
+ end
18
+
19
+ def as_json(*)
20
+ {
21
+ objectAction: :publicView,
22
+ objectType: @model.object_type,
23
+ fields: @model.object_fields.all,
24
+ values: @ids.map { |i| { @model.object_fields::FIELDS[:id] => i } }
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1 @@
1
+ class InvalidClient < StandardError; end
@@ -0,0 +1,94 @@
1
+ require_relative './invalid_client'
2
+
3
+ module RescueGroups
4
+ module Requests
5
+ class Where
6
+ attr_reader :search_engine, :results
7
+
8
+ def initialize(conditions, model, client, search_engine_class)
9
+ modifiers = %i[limit start sort order]
10
+
11
+ conditions.each do |key, value|
12
+ if modifiers.include?(key.to_sym)
13
+ instance_variable_set(:"@#{ key }", conditions.delete(key))
14
+ end
15
+ end
16
+
17
+ @conditions = conditions
18
+ @model = model
19
+ @client = client
20
+ @search_engine = build_search_engine(search_engine_class)
21
+ @results = { 'data' => {} }
22
+ end
23
+
24
+ def request
25
+ raise InvalidClient, 'Invalid client given to Requests::Where' unless @client.respond_to?(:post_and_respond)
26
+ response = @client.post_and_respond(as_json)
27
+
28
+ if response.success? && !response['data'].empty?
29
+ @results['found_rows'] = response['found_rows']
30
+ @results['data'].merge!(response['data'])
31
+ end
32
+
33
+ response
34
+ end
35
+
36
+ def as_json(*)
37
+ {
38
+ objectAction: :publicSearch,
39
+ objectType: @model.object_type,
40
+ search: @search_engine.as_json,
41
+ }
42
+ end
43
+
44
+ def update_conditions!(conditions)
45
+ @conditions.merge!(conditions)
46
+ end
47
+
48
+ def can_request_more?
49
+ return false if results.nil? || results['data'].nil? || results['data'].empty?
50
+ !results['found_rows'].nil? && results['data'].keys.length < results['found_rows']
51
+ end
52
+
53
+ private
54
+
55
+ def build_search_engine(search_engine_class)
56
+ args = {
57
+ limit: @limit,
58
+ start: @start,
59
+ sort: @sort,
60
+ }.reject { |_, v| v.nil? }
61
+
62
+ search = search_engine_class.new(**args)
63
+
64
+ add_filters_to_search_engine(search)
65
+
66
+ search
67
+ end
68
+
69
+ def add_filters_to_search_engine(search)
70
+ conditions_to_rescue_groups_key_value do |mapped_key, val|
71
+ equality_operator = :equal
72
+
73
+ if val.is_a?(Hash)
74
+ equality_operator = val.keys[0]
75
+ val = val.values[0]
76
+ end
77
+
78
+ search.add_filter(mapped_key, equality_operator, val)
79
+ end
80
+ end
81
+
82
+ def conditions_to_rescue_groups_key_value(&block)
83
+ fail('Block not given') unless block_given?
84
+ @conditions.each do |key, value|
85
+ mapped_key = @model.object_fields::FIELDS[key.to_sym]
86
+
87
+ next if mapped_key.nil?
88
+
89
+ yield mapped_key, value
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
data/lib/response.rb ADDED
@@ -0,0 +1,48 @@
1
+ module RescueGroups
2
+ class Response
3
+ attr_accessor :http_status_code, :parsed_body
4
+
5
+ # method: initialize
6
+ # purpose: When a Response is created, this method
7
+ # extracts the code and parsed_response from
8
+ # the raw HTTParty response
9
+ # param: raw_response - <HTTParty Response> - raw HTTParty response
10
+ # return: nil
11
+ def initialize(raw_response)
12
+ @http_status_code = raw_response.code
13
+ @parsed_body = raw_response.parsed_response
14
+ end
15
+
16
+ # method: []
17
+ # purpose: Convenience method to access an attribute from
18
+ # the parsed body
19
+ # param: attribute - <String> - key of desired value from response
20
+ # return: value at the specified key of the response
21
+ def [](attribute)
22
+ @parsed_body[attribute]
23
+ end
24
+
25
+ # method: success
26
+ # purpose: return true/false for if the response had an error (Non-HTTP error)
27
+ # param: none
28
+ # return: <Boolean> state of the response['status']
29
+ def success?
30
+ self['status'] != 'error'
31
+ end
32
+
33
+ # method: error
34
+ # purpose: traverse down response tree and extract the error
35
+ # text on errorful responses
36
+ # param: none
37
+ # return: <Nil> if there is no error
38
+ # <String> if an error exists and messageCriticality is expected
39
+ # value
40
+ def error
41
+ unless success?
42
+ self['messages']['generalMessages'].map do |m|
43
+ m['messageText'] if m['messageCriticality'] == 'error'
44
+ end.compact.join("\n")
45
+ end
46
+ end
47
+ end
48
+ end
data/models/animal.rb ADDED
@@ -0,0 +1,57 @@
1
+ require_relative '../lib/remote_model'
2
+ require_relative '../search/animal_search'
3
+
4
+ module RescueGroups
5
+ class Animal < RemoteModel
6
+ belongs_to :organization
7
+
8
+ class << self
9
+ # method :object_type
10
+ # purpose: Define this class's object_type used by the
11
+ # the Queryable module when composing remote queries
12
+ # param: none
13
+ # return: <Symbol> - the value of the object_type
14
+ def object_type
15
+ :animals
16
+ end
17
+
18
+ # method :object_fields
19
+ # purpose: Define this class's object fields class used by the
20
+ # the Queryable module when composing remote queries
21
+ # param: none
22
+ # return: <Constant> - the class containing the list of fields
23
+ # pertinent to this class
24
+ def object_fields
25
+ AnimalField
26
+ end
27
+
28
+ # method :search_engine_class
29
+ # purpose: Define which search engine is used by the class. The Queryable
30
+ # module uses the search engine when constructing remote queries to make
31
+ #
32
+ # param: none
33
+ # return: <Constant> - the class that contains search data pertinent to this class
34
+ def search_engine_class
35
+ AnimalSearch
36
+ end
37
+ end
38
+
39
+ def initialize(*args)
40
+ super
41
+
42
+ extract_pictures if !@pictures.nil? && !@pictures.empty?
43
+ end
44
+
45
+ attr_accessor *object_fields::FIELDS.keys
46
+
47
+ private
48
+
49
+ def extract_pictures
50
+ @pictures.map! do |picture_data|
51
+ Picture.new(picture_data).tap do |picture|
52
+ picture.animal_id = self.id
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
data/models/event.rb ADDED
@@ -0,0 +1,41 @@
1
+ require_relative '../lib/remote_model'
2
+ require_relative '../search/event_search'
3
+
4
+ module RescueGroups
5
+ class Event < RemoteModel
6
+ belongs_to :organization
7
+
8
+ class << self
9
+ # method :object_type
10
+ # purpose: Define this class's object_type used by the
11
+ # the Queryable module when composing remote queries
12
+ # param: none
13
+ # return: <Symbol> - the value of the object_type
14
+ def object_type
15
+ :events
16
+ end
17
+
18
+ # method :object_fields
19
+ # purpose: Define this class's object fields class used by the
20
+ # the Queryable module when composing remote queries
21
+ # param: none
22
+ # return: <Constant> - the class containing the list of fields
23
+ # pertinent to this class
24
+ def object_fields
25
+ EventField
26
+ end
27
+
28
+ # method :search_engine_class
29
+ # purpose: Define which search engine is used by the class. The Queryable
30
+ # module uses the search engine when constructing remote queries to make
31
+ #
32
+ # param: none
33
+ # return: <Constant> - the class that contains search data pertinent to this class
34
+ def search_engine_class
35
+ EventSearch
36
+ end
37
+ end
38
+
39
+ attr_accessor *object_fields::FIELDS.keys
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require_relative '../lib/remote_model'
2
+ require_relative '../search/organization_search'
3
+
4
+ module RescueGroups
5
+ class Organization < RemoteModel
6
+ has_many :animals
7
+
8
+ class << self
9
+ # method :object_type
10
+ # purpose: Define this class's object_type used by the
11
+ # the Queryable module when composing remote queries
12
+ # param: none
13
+ # return: <Symbol> - the value of the object_type
14
+ def object_type
15
+ :orgs
16
+ end
17
+
18
+ # method :object_fields
19
+ # purpose: Define this class's object fields class used by the
20
+ # the Queryable module when composing remote queries
21
+ # param: none
22
+ # return: <Constant> - the class containing the list of fields
23
+ # pertinent to this class
24
+ def object_fields
25
+ OrganizationField
26
+ end
27
+
28
+ # method :search_engine_class
29
+ # purpose: Define which search engine is used by the class. The Queryable
30
+ # module uses the search engine when constructing remote queries to make
31
+ #
32
+ # param: none
33
+ # return: <Constant> - the class that contains search data pertinent to this class
34
+ def search_engine_class
35
+ OrganizationSearch
36
+ end
37
+ end
38
+
39
+ attr_accessor *object_fields::FIELDS.keys
40
+ end
41
+ end
data/models/picture.rb ADDED
@@ -0,0 +1,26 @@
1
+ require_relative '../fields/picture_field'
2
+
3
+ module RescueGroups
4
+ class Picture
5
+ include Relationable
6
+
7
+ belongs_to :animal
8
+
9
+ attr_accessor *PictureField::FIELDS.values
10
+
11
+ def initialize(attribute_hash = {})
12
+ attribute_hash.each do |key, value|
13
+ mapped_key = PictureField::FIELDS[key.to_sym]
14
+ self.send(:"#{ mapped_key }=", value) unless mapped_key.nil?
15
+ end
16
+ end
17
+
18
+ def url(secure: false)
19
+ secure ? url_full : insecure_url_full
20
+ end
21
+
22
+ def url_thumb(secure: false)
23
+ secure ? url_thumbnail : insecure_url_thumb
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH << File.dirname(__FILE__)
2
+ require 'version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'rescue_groups'
6
+ s.version = RescueGroups::VERSION
7
+ s.summary = %q{RescueGroups.org API wrapper}
8
+ s.description = %q{Ruby ORM for the RescueGroups API. On demand pet data provided for free by RescueGroups.org.}
9
+ s.authors = ['Jake Yesbeck']
10
+ s.email = 'yesbeckjs@gmail.com'
11
+ s.homepage = 'http://rubygems.org/gems/rescue_groups'
12
+ s.license = 'MIT'
13
+
14
+ s.require_paths = %w(lib models search config)
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = s.files.grep(/^spec\//)
17
+
18
+ s.required_ruby_version = '>= 2.0.0'
19
+
20
+ s.add_dependency 'httparty', '>= 0.13.0'
21
+
22
+ s.add_development_dependency 'bundler', '~> 1.3'
23
+ s.add_development_dependency 'rake'
24
+ s.add_development_dependency 'rspec'
25
+ s.add_development_dependency 'pry'
26
+ s.add_development_dependency 'webmock'
27
+ s.add_development_dependency 'guard-rspec'
28
+ end