teamsnap_rb 1.3.3 → 2.0.0.beta

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 (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