teamsnap_rb 1.3.3 → 2.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +131 -29
  4. data/lib/config/inflecto.rb +13 -0
  5. data/lib/config/oj.rb +5 -0
  6. data/lib/teamsnap.rb +36 -376
  7. data/lib/teamsnap/api.rb +113 -0
  8. data/lib/teamsnap/auth_middleware.rb +62 -0
  9. data/lib/teamsnap/client.rb +51 -0
  10. data/lib/teamsnap/collection.rb +125 -0
  11. data/lib/teamsnap/item.rb +100 -0
  12. data/lib/teamsnap/response.rb +101 -0
  13. data/lib/teamsnap/structure.rb +80 -0
  14. data/lib/teamsnap/version.rb +1 -1
  15. data/spec/cassettes/apiv3-init.yml +756 -124
  16. data/spec/cassettes/client/when_calling_via_s_on_the_client/does_not_raise_an_error_when_the_HTTP_actions_are_called.yml +56 -0
  17. data/spec/cassettes/structure/_create_collection_class/registers_new_classes_via_introspection_of_the_root_collection.yml +110 -0
  18. data/spec/cassettes/structure/_create_collection_class/sets_the_href_attribute_on_the_new_class.yml +57 -0
  19. data/spec/cassettes/structure/_init/has_all_classes_in_schema_loaded_except_for_exceptions_list_endpoints.yml +56 -0
  20. data/spec/cassettes/structure/has_all_classes_in_schema_loaded_except_for_exceptions_list_endpoints.yml +56 -0
  21. data/spec/cassettes/teamsnap__client/when_calling_via_s_on_the_client/does_not_raise_an_error_when_the_HTTP_actions_are_called.yml +69 -0
  22. data/spec/cassettes/teamsnap__client/when_calling_via_s_on_the_client/passes_them_to_the_faraday_client_using_method_missing.yml +69 -0
  23. data/spec/cassettes/teamsnap__collection/adds_find_if_search_is_available.yml +61 -0
  24. data/spec/cassettes/teamsnap__collection/adds_href_to_items.yml +62 -0
  25. data/spec/cassettes/teamsnap__collection/can_follow_plural_links.yml +179 -0
  26. data/spec/cassettes/teamsnap__collection/can_follow_singular_links.yml +120 -0
  27. data/spec/cassettes/teamsnap__collection/can_handle_links_with_no_data.yml +107 -0
  28. data/spec/cassettes/{teamsnap_rb/can_handle_errors_generated_by_command.yml → teamsnap__collection/can_handle_no_argument_errors_generated_by_command.yml} +10 -16
  29. data/spec/cassettes/teamsnap__collection/handles_executing_an_action_via_commands.yml +64 -0
  30. data/spec/cassettes/teamsnap__collection/handles_executing_an_action_via_commands_with_multiple_params.yml +70 -0
  31. data/spec/cassettes/teamsnap__collection/handles_fetching_data_via_queries.yml +61 -0
  32. data/spec/cassettes/teamsnap__collection/handles_queries_with_no_data.yml +57 -0
  33. data/spec/cassettes/teamsnap__collection/raises_an_exception_if_find_returns_nothing.yml +57 -0
  34. data/spec/cassettes/teamsnap__collection/supports_relations_with_expected_behaviors/when_a_plural_relation_is_called/responds_with_an_array_of_objects_when_successful.yml +117 -0
  35. data/spec/cassettes/teamsnap__collection/supports_relations_with_expected_behaviors/when_a_plural_relation_is_called/responds_with_an_empty_array_when_no_objects_exist.yml +111 -0
  36. data/spec/cassettes/teamsnap__collection/supports_relations_with_expected_behaviors/when_a_singular_relation_is_called/responds_with_nil_if_it_does_NOT_exist.yml +111 -0
  37. data/spec/cassettes/teamsnap__collection/supports_relations_with_expected_behaviors/when_a_singular_relation_is_called/responds_with_the_object_if_it_exists.yml +124 -0
  38. data/spec/cassettes/teamsnap__structure/_create_collection_class/registers_new_classes_via_introspection_of_the_root_collection.yml +57 -0
  39. data/spec/cassettes/teamsnap__structure/_create_collection_class/sets_the_href_attribute_on_the_new_class.yml +57 -0
  40. data/spec/cassettes/teamsnap__structure/_init/has_all_classes_in_schema_loaded_except_for_exceptions_list_endpoints.yml +69 -0
  41. data/spec/cassettes/teamsnap_rb/_bulk_load/can_handle_an_empty_bulk_load.yml +55 -0
  42. data/spec/cassettes/teamsnap_rb/{can_handle_an_error_with_bulk_load.yml → _bulk_load/can_handle_an_error_with_bulk_load.yml} +11 -23
  43. data/spec/cassettes/teamsnap_rb/_bulk_load/can_use_bulk_load.yml +121 -0
  44. data/spec/cassettes/teamsnap_rb/_client_send/when_sent_a_known_via_/calls_DELETE_on_the_given_client.yml +2405 -0
  45. data/spec/cassettes/teamsnap_rb/_client_send/when_sent_a_known_via_/calls_GET_on_the_given_client.yml +69 -0
  46. data/spec/cassettes/teamsnap_rb/_client_send/when_sent_a_known_via_/calls_PATCH_on_the_given_client.yml +2404 -0
  47. data/spec/cassettes/teamsnap_rb/_client_send/when_sent_a_known_via_/calls_POST_on_the_given_client.yml +2404 -0
  48. data/spec/cassettes/teamsnap_rb/_run/processes_the_response.yml +267 -0
  49. data/spec/cassettes/teamsnap_rb/adds_find_if_search_is_available.yml +27 -30
  50. data/spec/cassettes/teamsnap_rb/adds_href_to_items.yml +31 -19
  51. data/spec/cassettes/teamsnap_rb/can_follow_plural_links.yml +118 -67
  52. data/spec/cassettes/teamsnap_rb/can_follow_singular_links.yml +59 -56
  53. data/spec/cassettes/teamsnap_rb/can_handle_links_with_no_data.yml +48 -38
  54. data/spec/cassettes/teamsnap_rb/can_handle_no_argument_errors_generated_by_command.yml +42 -0
  55. data/spec/cassettes/teamsnap_rb/handles_executing_an_action_via_commands.yml +32 -20
  56. data/spec/cassettes/teamsnap_rb/handles_executing_an_action_via_commands_with_multiple_params.yml +29 -404
  57. data/spec/cassettes/teamsnap_rb/handles_fetching_data_via_queries.yml +26 -23
  58. data/spec/cassettes/teamsnap_rb/handles_queries_with_no_data.yml +24 -28
  59. data/spec/cassettes/teamsnap_rb/raises_an_exception_if_find_returns_nothing.yml +24 -28
  60. data/spec/cassettes/teamsnap_rb/supports_relations_with_expected_behaviors/when_a_plural_relation_is_called/responds_with_an_array_of_objects_when_successful.yml +53 -45
  61. data/spec/cassettes/teamsnap_rb/supports_relations_with_expected_behaviors/when_a_plural_relation_is_called/responds_with_an_empty_array_when_no_objects_exist.yml +50 -54
  62. data/spec/cassettes/teamsnap_rb/supports_relations_with_expected_behaviors/when_a_singular_relation_is_called/responds_with_nil_if_it_does_NOT_exist.yml +50 -54
  63. data/spec/cassettes/teamsnap_rb/supports_relations_with_expected_behaviors/when_a_singular_relation_is_called/responds_with_the_object_if_it_exists.yml +59 -58
  64. data/spec/spec_helper.rb +2 -0
  65. data/spec/teamsnap/client_spec.rb +75 -0
  66. data/spec/teamsnap/collection_spec.rb +155 -0
  67. data/spec/teamsnap/item_spec.rb +155 -0
  68. data/spec/teamsnap/structure_spec.rb +63 -0
  69. data/spec/teamsnap_spec.rb +169 -157
  70. data/teamsnap_rb.gemspec +1 -1
  71. metadata +92 -15
  72. data/spec/cassettes/teamsnap_rb/can_handle_an_empty_bulk_load.yml +0 -60
  73. data/spec/cassettes/teamsnap_rb/can_use_bulk_load.yml +0 -74
@@ -0,0 +1,113 @@
1
+ module TeamSnap
2
+ class Api
3
+
4
+ CRUD_METHODS = [:find, :create, :update, :delete]
5
+ CRUD_VIAS = [:get, :post, :patch, :delete]
6
+
7
+ def self.run(client, method, klass, args = {}, template_args = false)
8
+ klass = klass.class == Symbol ? get_class(klass) : klass
9
+ via = via(klass, method)
10
+ href = href(klass.href, method, args)
11
+ args = args(method, args)
12
+ client_send_args = template_args ? template_attributes(args) : args
13
+ resp = TeamSnap.client_send(client, via, href, client_send_args)
14
+ TeamSnap::Response.new(
15
+ :args => args,
16
+ :client => client,
17
+ :client_send_args => client_send_args,
18
+ :href => href,
19
+ :resp => resp,
20
+ :status => resp.status,
21
+ :via => via
22
+ )
23
+ end
24
+
25
+ def self.args(method, sent_args)
26
+ case method
27
+ when :update
28
+ sent_args.except(:id)
29
+ when :find, :delete
30
+ {}
31
+ else
32
+ sent_args
33
+ end
34
+ end
35
+
36
+ def self.get_class(sym)
37
+ "TeamSnap::#{sym.to_s.singularize.camelcase}".constantize
38
+ end
39
+
40
+ def self.href(base_href, method, args = {})
41
+ case method
42
+ when :find, :delete
43
+ if [Fixnum, String].include?(args.class)
44
+ base_href + "/#{args}"
45
+ elsif args.class == Hash
46
+ base_href + "/#{args.fetch(:id)}"
47
+ else
48
+ raise TeamSnap::Error.new("You must pass in the `id` of the object you would like to :find or :delete")
49
+ end
50
+ when :create
51
+ base_href
52
+ when :update
53
+ base_href + "/#{args.fetch(:id)}"
54
+ else
55
+ base_href + "/#{method}"
56
+ end
57
+ end
58
+
59
+ def self.via(klass, method)
60
+ queries = klass.query_names
61
+ commands = klass.command_names
62
+
63
+ method_map = CRUD_METHODS + queries + commands
64
+ via_map = CRUD_VIAS + ([:get] * queries.count) + ([:post] * commands.count)
65
+
66
+ # SET VIA
67
+ if method_index = method_map.index(method)
68
+ return via_map[method_index]
69
+ else
70
+ raise TeamSnap::Error.new("Method Missing: `#{method}` for Collection Class: `#{klass}`")
71
+ end
72
+ end
73
+
74
+ def self.parse_error(resp)
75
+ return "Object Not Found (404)" if resp.status == 404
76
+ begin
77
+ Oj.load(resp.body)
78
+ .fetch(:collection)
79
+ .fetch(:error)
80
+ .fetch(:message)
81
+ rescue KeyError
82
+ resp.body
83
+ end
84
+ end
85
+
86
+ def self.template_args?(method)
87
+ [:create, :update].include?(method)
88
+ end
89
+
90
+ def self.template_attributes(attributes)
91
+ request_attributes = {
92
+ :template => {
93
+ :data => []
94
+ }
95
+ }
96
+ attributes.each do |key, value|
97
+ request_attributes[:template][:data] << {
98
+ "name" => key,
99
+ "value" => value
100
+ }
101
+ end
102
+ return request_attributes
103
+ end
104
+
105
+ def self.untemplate_attributes(request_attributes)
106
+ attributes = {}
107
+ request_attributes.fetch(:template).fetch(:data).each do |datum|
108
+ attributes[datum.fetch(:name).to_sym] = datum.fetch(:value)
109
+ end
110
+ return attributes
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ module TeamSnap
2
+ class AuthMiddleware < Faraday::Middleware
3
+ def initialize(app, options)
4
+ @options = options
5
+ super(app)
6
+ end
7
+
8
+ def call(env)
9
+ if token
10
+ env[:request_headers].merge!({"Authorization" => "Bearer #{token}"})
11
+ elsif client_id && client_secret
12
+ query_params = Hash[URI.decode_www_form(env.url.query || "")]
13
+ .merge({
14
+ hmac_client_id: client_id,
15
+ hmac_nonce: SecureRandom.uuid,
16
+ hmac_timestamp: Time.now.to_i
17
+ })
18
+ env.url.query = URI.encode_www_form(query_params)
19
+
20
+ env.request_headers["X-Teamsnap-Hmac"] = OpenSSL::HMAC.hexdigest(
21
+ digest, client_secret, message_hash(env)
22
+ )
23
+ end
24
+
25
+ @app.call(env)
26
+ end
27
+
28
+ def token
29
+ @token ||= @options[:token]
30
+ end
31
+
32
+ def client_id
33
+ @client_id ||= @options[:client_id]
34
+ end
35
+
36
+ def client_secret
37
+ @client_secret ||= @options[:client_secret]
38
+ end
39
+
40
+ def digest
41
+ OpenSSL::Digest.new("sha256")
42
+ end
43
+
44
+ def message_hash(env)
45
+ digest.hexdigest(
46
+ query_string(env) + message(env)
47
+ )
48
+ end
49
+
50
+ def query_string(env)
51
+ "/?" + env.url.query.to_s
52
+ end
53
+
54
+ def message(env)
55
+ env.body || ""
56
+ end
57
+ end
58
+ end
59
+
60
+ Faraday::Request.register_middleware(
61
+ :teamsnap_auth_middleware => -> { TeamSnap::AuthMiddleware }
62
+ )
@@ -0,0 +1,51 @@
1
+ module TeamSnap
2
+ class Client
3
+
4
+ class << self
5
+ def set_faraday_client(url, token, client_id, client_secret)
6
+ Faraday.new(
7
+ :url => url,
8
+ :parallel_manager => Typhoeus::Hydra.new
9
+ ) do |c|
10
+ c.request :teamsnap_auth_middleware, {
11
+ :token => token,
12
+ :client_id => client_id,
13
+ :client_secret => client_secret
14
+ }
15
+ c.adapter :typhoeus
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_accessor :faraday_client
21
+
22
+ def initialize(opts = {})
23
+ c_url = opts.fetch(:url) {}
24
+ c_token = opts.fetch(:token) {}
25
+ c_id = opts.fetch(:client_id) {}
26
+ c_secret = opts.fetch(:client_secret) {}
27
+
28
+ self.faraday_client = TeamSnap::Client.set_faraday_client(
29
+ c_url || TeamSnap.url,
30
+ c_token,
31
+ c_id || TeamSnap.client_id,
32
+ c_secret || TeamSnap.client_secret
33
+ )
34
+ end
35
+
36
+ def method_missing(method, *args, &block)
37
+ self.faraday_client.send(method, *args, &block)
38
+ end
39
+
40
+ def api(method, klass, sent_args = {})
41
+ TeamSnap::Api.run(
42
+ self,
43
+ method,
44
+ klass,
45
+ sent_args,
46
+ TeamSnap::Api.template_args?(method)
47
+ )
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,125 @@
1
+ module TeamSnap
2
+ module Collection
3
+
4
+ class << self
5
+
6
+ def apply_endpoints(obj, collection)
7
+ queries = collection.fetch(:queries) { [] }
8
+ commands = collection.fetch(:commands) { [] }
9
+
10
+ endpoint_creation_set(obj, queries, :get)
11
+ endpoint_creation_set(obj, commands, :post)
12
+ end
13
+
14
+ def endpoint_creation_set(obj, creation_set, via)
15
+ creation_set.each{ |endpoint| register_endpoint(obj, endpoint, :via => via) }
16
+ end
17
+
18
+ def register_endpoint(obj, endpoint, opts)
19
+ rel = endpoint.fetch(:rel)
20
+ href = endpoint.fetch(:href)
21
+ valid_args = endpoint.fetch(:data) { [] }
22
+ .map { |datum| datum.fetch(:name).to_sym }
23
+ via = opts.fetch(:via)
24
+
25
+ obj.define_singleton_method(rel) do |client, *args|
26
+ args = Hash[*args]
27
+
28
+ unless args.all? { |arg, _| valid_args.include?(arg) }
29
+ raise ArgumentError.new(
30
+ "Invalid argument(s). Valid argument(s) are #{valid_args.inspect}"
31
+ )
32
+ end
33
+
34
+ resp = TeamSnap.run(client, via, href, args)
35
+ TeamSnap::Item.load_items(client, resp)
36
+ end
37
+ end
38
+ end
39
+
40
+ def actions
41
+ actions = parsed_collection.fetch(:actions) {
42
+ %w(create read update delete search)
43
+ }
44
+ return actions.map(&:to_sym)
45
+ end
46
+
47
+ def queries
48
+ parsed_collection.fetch(:queries) { [] }
49
+ end
50
+
51
+ def query_names
52
+ queries.map{ |q| q[:rel].to_sym }
53
+ end
54
+
55
+ def commands
56
+ parsed_collection.fetch(:commands) { [] }
57
+ end
58
+
59
+ def command_names
60
+ commands.map{ |q| q[:rel].to_sym }
61
+ end
62
+
63
+ def create(client, attributes = {})
64
+ post_attributes = TeamSnap::Api.template_attributes(attributes)
65
+
66
+ create_resp = TeamSnap.run(client, :post, href, post_attributes)
67
+ TeamSnap::Item.load_items(client, create_resp).first
68
+ end
69
+
70
+ def update(client, id, attributes = {})
71
+ patch_attributes = TeamSnap::Api.template_attributes(attributes)
72
+
73
+ update_resp = TeamSnap.run(client, :patch, href+"/#{id}", patch_attributes)
74
+ TeamSnap::Item.load_items(client, update_resp).first
75
+ end
76
+
77
+ def delete(client, id)
78
+ TeamSnap.run(client, :delete, href+"/#{id}", {})
79
+ end
80
+
81
+ def template_attributes
82
+ template = parsed_collection.fetch(:template) {}
83
+ data = template.fetch(:data) { [] }
84
+ data
85
+ .reject{ |col| col.fetch(:name) == "type" }
86
+ .map{ |col| col.fetch(:name) }
87
+ end
88
+
89
+ def href
90
+ self.instance_variable_get(:@href)
91
+ end
92
+
93
+ def resp
94
+ self.instance_variable_get(:@resp)
95
+ end
96
+
97
+ def parsed_collection
98
+ self.instance_variable_get(:@parsed_collection)
99
+ end
100
+
101
+ def parse_collection
102
+ if resp
103
+ TeamSnap.response_check(resp, :get)
104
+ collection = Oj.load(resp.body).fetch(:collection) { [] }
105
+ elsif parsed_collection
106
+ collection = parsed_collection
107
+ end
108
+
109
+ TeamSnap::Collection.apply_endpoints(self, collection)
110
+ enable_find if respond_to?(:search)
111
+ end
112
+
113
+ private
114
+
115
+ def enable_find
116
+ define_singleton_method(:find) do |client, id|
117
+ search(client, :id => id).first.tap do |object|
118
+ raise TeamSnap::NotFound.new(
119
+ "Could not find a #{self} with an id of '#{id}'."
120
+ ) unless object
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,100 @@
1
+ module TeamSnap
2
+ module Item
3
+
4
+ class << self
5
+ def load_items(client, collection)
6
+ collection
7
+ .fetch(:items) { [] }
8
+ .map { |item|
9
+ data = parse_data(item).merge(:href => item[:href])
10
+ type = type_of(item)
11
+ cls = load_class(type, data)
12
+
13
+ cls.new(data).tap { |obj|
14
+ obj.send(:load_links, client, item.fetch(:links) { [] })
15
+ }
16
+ }
17
+ end
18
+
19
+ def parse_data(item)
20
+ data = item
21
+ .fetch(:data)
22
+ .map { |datum|
23
+ name = datum.fetch(:name)
24
+ value = datum.fetch(:value)
25
+ type = datum.fetch(:type) { :default }
26
+
27
+ value = DateTime.parse(value) if value && type == "DateTime"
28
+
29
+ [name, value]
30
+ }
31
+ hashify(data)
32
+ end
33
+
34
+ def type_of(item)
35
+ item
36
+ .fetch(:data)
37
+ .find { |datum| datum.fetch(:name) == "type" }
38
+ .fetch(:value)
39
+ end
40
+
41
+ def load_class(type, data)
42
+ TeamSnap.const_get(Inflecto.camelize(type), false).tap { |cls|
43
+ unless cls.include?(Virtus::Model::Core)
44
+ cls.class_eval do
45
+ include Virtus.value_object
46
+
47
+ values do
48
+ attribute :href, String
49
+ data.each { |name, value| attribute name, value.class }
50
+ end
51
+ end
52
+ end
53
+ }
54
+ end
55
+
56
+ def hashify(arr)
57
+ Hash[*arr.flatten]
58
+ rescue NoMethodError
59
+ arr.inject({}) { |hash, (key, value)| hash[key] = value; hash }
60
+ end
61
+
62
+ end
63
+
64
+ private
65
+
66
+ def load_links(client, links)
67
+ links.each do |link|
68
+ next if EXCLUDED_RELS.include?(link.fetch(:rel))
69
+
70
+ rel = link.fetch(:rel)
71
+ href = link.fetch(:href)
72
+ is_singular = rel == Inflecto.singularize(rel)
73
+
74
+ define_singleton_method(rel) {
75
+ instance_variable_get("@#{rel}") || instance_variable_set(
76
+ "@#{rel}", -> {
77
+ coll = TeamSnap::Item.load_items(
78
+ client,
79
+ TeamSnap.run(client, :get, href)
80
+ )
81
+ is_singular ? coll.first : coll
82
+ }.call
83
+ )
84
+ }
85
+ end
86
+
87
+ define_singleton_method(:update) { |attributes|
88
+ patch_attributes = TeamSnap::Api.template_attributes(attributes)
89
+
90
+ response = TeamSnap.run(client, :patch, instance_variable_get("@href"), patch_attributes)
91
+ TeamSnap::Item.load_items(client, response).first
92
+ }
93
+
94
+ define_singleton_method(:delete) {
95
+ response = TeamSnap.run(client, :delete, instance_variable_get("@href"), {})
96
+ TeamSnap::Item.load_items(client, response).first
97
+ }
98
+ end
99
+ end
100
+ end