cloud_spokes_api_model 0.1.0

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 (44) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +38 -0
  4. data/app/config/config.yml +16 -0
  5. data/app/models/active_model/model.rb +99 -0
  6. data/app/models/cloud_spokes/model/base_api.rb +198 -0
  7. data/app/models/cloud_spokes/model/category.rb +12 -0
  8. data/app/models/cloud_spokes/model/member.rb +32 -0
  9. data/lib/cloud_spokes_api_model/engine.rb +5 -0
  10. data/lib/cloud_spokes_api_model/railtie.rb +15 -0
  11. data/lib/cloud_spokes_api_model/version.rb +3 -0
  12. data/lib/cloud_spokes_api_model.rb +7 -0
  13. data/lib/tasks/cloud_spokes_api_model_tasks.rake +4 -0
  14. data/test/cloud_spokes_api_model_test.rb +7 -0
  15. data/test/dummy/README.rdoc +261 -0
  16. data/test/dummy/Rakefile +7 -0
  17. data/test/dummy/app/assets/javascripts/application.js +15 -0
  18. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  19. data/test/dummy/app/controllers/application_controller.rb +3 -0
  20. data/test/dummy/app/helpers/application_helper.rb +2 -0
  21. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  22. data/test/dummy/config/application.rb +59 -0
  23. data/test/dummy/config/boot.rb +10 -0
  24. data/test/dummy/config/database.yml +25 -0
  25. data/test/dummy/config/environment.rb +5 -0
  26. data/test/dummy/config/environments/development.rb +37 -0
  27. data/test/dummy/config/environments/production.rb +67 -0
  28. data/test/dummy/config/environments/test.rb +37 -0
  29. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  30. data/test/dummy/config/initializers/inflections.rb +15 -0
  31. data/test/dummy/config/initializers/mime_types.rb +5 -0
  32. data/test/dummy/config/initializers/secret_token.rb +7 -0
  33. data/test/dummy/config/initializers/session_store.rb +8 -0
  34. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  35. data/test/dummy/config/locales/en.yml +5 -0
  36. data/test/dummy/config/routes.rb +58 -0
  37. data/test/dummy/config.ru +4 -0
  38. data/test/dummy/public/404.html +26 -0
  39. data/test/dummy/public/422.html +26 -0
  40. data/test/dummy/public/500.html +25 -0
  41. data/test/dummy/public/favicon.ico +0 -0
  42. data/test/dummy/script/rails +6 -0
  43. data/test/test_helper.rb +15 -0
  44. metadata +204 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012 YOURNAME
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,3 @@
1
+ = CloudSpokesApiModel
2
+
3
+ This project rocks and uses MIT-LICENSE.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'CloudSpokesApiModel'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,16 @@
1
+ defaults: &defaults
2
+ :cs_api:
3
+ :challenges: 'http://cs-api-sandbox.herokuapp.com/v1/challenges'
4
+ :categories: 'http://cs-api-sandbox.herokuapp.com/v1/categories'
5
+ :members: 'http://cs-api-sandbox.herokuapp.com/v1/members'
6
+ :key: ''
7
+ :expiry: 5
8
+
9
+ development:
10
+ <<: *defaults
11
+
12
+ test:
13
+ <<: *defaults
14
+
15
+ production:
16
+ <<: *defaults
@@ -0,0 +1,99 @@
1
+ # shamelessly stolen from https://github.com/rails/rails/tree/master/activemodel :P
2
+
3
+ module ActiveModel
4
+
5
+ # == Active \Model Basic \Model
6
+ #
7
+ # Includes the required interface for an object to interact with
8
+ # <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
9
+ # It includes model name introspections, conversions, translations and
10
+ # validations. Besides that, it allows you to initialize the object with a
11
+ # hash of attributes, pretty much like <tt>ActiveRecord</tt> does.
12
+ #
13
+ # A minimal implementation could be:
14
+ #
15
+ # class Person
16
+ # include ActiveModel::Model
17
+ # attr_accessor :name, :age
18
+ # end
19
+ #
20
+ # person = Person.new(name: 'bob', age: '18')
21
+ # person.name # => 'bob'
22
+ # person.age # => 18
23
+ #
24
+ # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
25
+ # to return +false+, which is the most common case. You may want to override
26
+ # it in your class to simulate a different scenario:
27
+ #
28
+ # class Person
29
+ # include ActiveModel::Model
30
+ # attr_accessor :id, :name
31
+ #
32
+ # def persisted?
33
+ # self.id == 1
34
+ # end
35
+ # end
36
+ #
37
+ # person = Person.new(id: 1, name: 'bob')
38
+ # person.persisted? # => true
39
+ #
40
+ # Also, if for some reason you need to run code on <tt>initialize</tt>, make
41
+ # sure you call +super+ if you want the attributes hash initialization to
42
+ # happen.
43
+ #
44
+ # class Person
45
+ # include ActiveModel::Model
46
+ # attr_accessor :id, :name, :omg
47
+ #
48
+ # def initialize(attributes={})
49
+ # super
50
+ # @omg ||= true
51
+ # end
52
+ # end
53
+ #
54
+ # person = Person.new(id: 1, name: 'bob')
55
+ # person.omg # => true
56
+ #
57
+ # For more detailed information on other functionalities available, please
58
+ # refer to the specific modules included in <tt>ActiveModel::Model</tt>
59
+ # (see below).
60
+ module Model
61
+ def self.included(base) #:nodoc:
62
+ base.class_eval do
63
+ extend ActiveModel::Naming
64
+ extend ActiveModel::Translation
65
+ include ActiveModel::Validations
66
+ include ActiveModel::Conversion
67
+ end
68
+ end
69
+
70
+ # Initializes a new model with the given +params+.
71
+ #
72
+ # class Person
73
+ # include ActiveModel::Model
74
+ # attr_accessor :name, :age
75
+ # end
76
+ #
77
+ # person = Person.new(name: 'bob', age: '18')
78
+ # person.name # => "bob"
79
+ # person.age # => 18
80
+ def initialize(params={})
81
+ params.each do |attr, value|
82
+ self.public_send("#{attr}=", value)
83
+ end if params
84
+ end
85
+
86
+ # Indicates if the model is persisted. Default is +false+.
87
+ #
88
+ # class Person
89
+ # include ActiveModel::Model
90
+ # attr_accessor :id, :name
91
+ # end
92
+ #
93
+ # person = Person.new(id: 1, name: 'bob')
94
+ # person.persisted? # => false
95
+ def persisted?
96
+ false
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,198 @@
1
+ class CloudSpokes::Model::BaseApi
2
+ include ActiveModel::Model
3
+
4
+ ENDPOINT_EXPIRY = ENV['CS_API_EXPIRY'].to_i || CloudSpokes::APP_CONFIG[:expiry]
5
+
6
+ cattr_accessor :access_token
7
+
8
+ # Implements the has_many relationship
9
+ # Passing :parent as an option allows modification of the calling class
10
+ # This is used mostly for has and belongs to many relationships, where
11
+ # a model collection will have a different endpoint
12
+ # Case in point: Members and Challenges
13
+ def self.has_many(entity, options={})
14
+ # add in this relationship to the column_names table
15
+ @column_names << entity.to_sym
16
+ rel_column_names << entity.to_sym
17
+ parent = options[:parent]
18
+
19
+ # dynamically create a method on this instance that will reference the collection
20
+ define_method("#{entity.to_sym}=") do |accessor_value|
21
+ instance_variable_set("@#{entity.to_sym}", accessor_value)
22
+ end
23
+
24
+ define_method(entity.to_sym) do
25
+ klass = entity.to_s.classify.constantize
26
+ (parent || klass).raw_get([to_param, entity.to_s]).map do |e|
27
+ next if e.respond_to?(:last) # we got an array instead of a Hashie::Mash
28
+ klass.new e
29
+ end
30
+ end
31
+ end
32
+
33
+ # Overrides the attr_accesssor class method so we are able to capture and
34
+ # then save the defined fields as column_names
35
+ def self.attr_accessor(*vars)
36
+ @column_names ||= []
37
+ @column_names.concat( vars )
38
+ super
39
+ end
40
+
41
+ # Returns the previously defined attr_accessor fields
42
+ def self.column_names
43
+ @column_names
44
+ end
45
+
46
+ def self.rel_column_names
47
+ @rel_column_names ||= []
48
+ end
49
+
50
+ # Returns the api_endpoint. Note that you need to implement this method
51
+ # in the child object
52
+ def self.api_endpoint
53
+ raise 'Please implement ::api_endpoint in the child object'
54
+ end
55
+
56
+ # Returns all the records from the CloudSpokes API
57
+ def self.all
58
+ request(:get, '', {}).map {|item| new item}
59
+ end
60
+
61
+ # Returns the first record
62
+ # TODO: when the API supports it, simply request for the first record.
63
+ # There should be no need to request for all the records first.
64
+ def self.first
65
+ all.first
66
+ end
67
+
68
+ # Finds an entity
69
+ def self.find(entity)
70
+ Rails.logger.info "========== running find for #{entity} for #{self.name}"
71
+ Kernel.const_get(self.name).new(raw_get entity)
72
+ end
73
+
74
+ # Wrap initialize with a sanitation clause
75
+ def initialize(params={})
76
+ Rails.logger.info "=====calling init with #{params}"
77
+ @raw_data = params.dup
78
+ params.delete_if {|k, v| !self.class.column_names.include? k.to_sym}
79
+ super(params)
80
+ end
81
+
82
+ # Returns the raw data that created this object
83
+ def raw_data
84
+ @raw_data
85
+ end
86
+
87
+ # Returns if this record has the id attribute set (used by url_for for routing)
88
+ def persisted?
89
+ !!id
90
+ end
91
+
92
+ def save
93
+ new_record? ? create : update
94
+ end
95
+
96
+ def update_attributes(attrs={})
97
+ attrs.each do |attr, value|
98
+ self.public_send("#{attr}=", value)
99
+ end
100
+ save
101
+ end
102
+
103
+ def new_record?
104
+ id.blank?
105
+ end
106
+
107
+ def self.create(attrs)
108
+ obj = new(attrs)
109
+ obj.save
110
+ obj
111
+ end
112
+
113
+ def self.api_request_headers
114
+ {
115
+ 'oauth_token' => access_token,
116
+ 'Authorization' => 'Token token="'+(ENV['CS_API_KEY']|| CloudSpokes::APP_CONFIG[:cs_api][:key])+'"',
117
+ 'Content-Type' => 'application/json'
118
+ }
119
+ end
120
+
121
+ # Convenience method to request an entity from the CloudSpokes RESTful source
122
+ # Accepts an array or a string
123
+ # If given an array, will join the elements with '/'
124
+ # If given a string, will use the argument as is
125
+ def self.raw_get(entities = [])
126
+ endpoint = endpoint_from_entities(entities)
127
+ Rails.logger.info "=====$$$$$ CALLING RAW GET #{entities} for #{endpoint}"
128
+ #Rails.cache.fetch("#{endpoint}", expires_in: ENDPOINT_EXPIRY.minutes) do
129
+ get_response(RestClient.get(endpoint, api_request_headers))
130
+ #end
131
+ end
132
+
133
+ # Sanitized response to only the attributes we've defined
134
+ def self.get(entity = '')
135
+ raw_get(entity).delete_if {|k, v| !column_names.include? k.to_sym}
136
+ end
137
+
138
+ def self.endpoint_from_entities(entities = [])
139
+ entities = entities.respond_to?(:join) ? entities.join("/") : entities.to_s
140
+ entities.present? ? "#{api_endpoint}/#{entities}" : api_endpoint
141
+ end
142
+
143
+ def self.get_response(data)
144
+ Hashie::Mash.new(JSON.parse(data)).response
145
+ end
146
+
147
+ def self.request(method, entities, data)
148
+ Rails.logger.info "===== access_token: #{access_token}"
149
+ Rails.logger.info "===== method: #{method}"
150
+ endpoint = endpoint_from_entities(entities)
151
+ Rails.logger.info "===== endpoint: #{endpoint}"
152
+ if method.to_sym == :get
153
+ endpoint += "?#{data.to_param}"
154
+ resp = RestClient.send method, endpoint, api_request_headers
155
+ else
156
+ data = data.to_json unless data.is_a?(String)
157
+ resp = RestClient.send method, endpoint, data, api_request_headers
158
+ end
159
+ get_response(resp)
160
+ end
161
+
162
+ def self.post(entities, data)
163
+ request :post, entities, data
164
+ end
165
+
166
+ def self.put(entities, data)
167
+ request :put, entities, data
168
+ end
169
+
170
+ private
171
+
172
+ def save_data
173
+ columns = self.class.column_names - self.class.rel_column_names - [:id]
174
+ columns.inject({}) do |ret, column|
175
+ val = self.public_send(column)
176
+ ret[column] = val if val.present?
177
+ ret
178
+ end
179
+ end
180
+
181
+ # define update_endpoint to subclass if you want to use another url for update
182
+ def update_endpoint
183
+ id
184
+ end
185
+
186
+ # define create_endpoint to subclass if you want to use another url for create
187
+ def create_endpoint
188
+ ""
189
+ end
190
+
191
+ def update
192
+ self.class.put update_endpoint, save_data
193
+ end
194
+
195
+ def create
196
+ self.class.post create_endpoint, save_data
197
+ end
198
+ end
@@ -0,0 +1,12 @@
1
+ class CloudSpokes::Model::Category < CloudSpokes::Model::BaseApi
2
+ attr_accessor :id, :attributes,
3
+ :name, :color
4
+
5
+ def self.api_endpoint
6
+ CloudSpokes::APP_CONFIG[:cs_api][:categories]
7
+ end
8
+
9
+ def self.names
10
+ all.map {|category| category.name}
11
+ end
12
+ end
@@ -0,0 +1,32 @@
1
+ class CloudSpokes::Model::Member < CloudSpokes::Model::BaseApi
2
+ attr_accessor :id, :name, :profile_pic, :attributes,
3
+ :challenges_entered, :active_challenges, :time_zone,
4
+ :total_1st_place, :total_2nd_place, :total_3st_place,
5
+ :total_wins, :total_public_money, :total_points, :valid_submissions,
6
+ :summary_bio, :payments
7
+
8
+ has_many :recommendations
9
+ has_many :challenges, parent: CloudSpokes::Model::Member
10
+
11
+ def self.api_endpoint
12
+ CloudSpokes::APP_CONFIG[:cs_api][:members]
13
+ end
14
+
15
+ # Used for resourceful routes (instead of id)
16
+ def to_param
17
+ name
18
+ end
19
+
20
+ # has_many :payments
21
+ # Note that the json does not expose this method
22
+ # TODO (this requires authentication)
23
+ def payments
24
+ 'nil'
25
+ end
26
+
27
+ def self.search(keyword)
28
+ request(:get, "search", {:keyword => keyword})
29
+ .map {|member| CloudSpokes::Model::Member.new member}
30
+ end
31
+
32
+ end
@@ -0,0 +1,5 @@
1
+ module CloudSpokes
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace CloudSpokes
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module CloudSpokes
2
+ class Railtie < Rails::Railtie
3
+
4
+ initializer "cloud_spokes.configure_rails_initialization" do
5
+ CloudSpokes::APP_CONFIG =
6
+ begin
7
+ YAML.load_file("#{Rails.root}/config/config.yml")[Rails.env]
8
+ rescue Errno::ENOENT => e
9
+ gem_config = $LOAD_PATH.find {|dir| dir =~ /app\/config/}
10
+ YAML.load_file("#{gem_config}/config.yml")[Rails.env]
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module CloudSpokes
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ require "cloud_spokes_api_model/engine"
2
+ require 'cloud_spokes_api_model/railtie' if defined?(Rails)
3
+ require 'hashie'
4
+ require 'rest_client'
5
+
6
+ module CloudSpokes
7
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :cloud_spokes_api_model do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class CloudSpokesApiModelTest < ActiveSupport::TestCase
4
+ test "truth" do
5
+ assert_kind_of Module, CloudSpokesApiModel
6
+ end
7
+ end