flareshow 0.3.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.
data/lib/invitation.rb ADDED
@@ -0,0 +1,16 @@
1
+ class Invitation < Flareshow::Resource
2
+
3
+ @read_only=true
4
+
5
+ # =================
6
+ # = Class Methods =
7
+ # =================
8
+ class << self
9
+ # invitations dont have timestamps currently
10
+ # overriding the default
11
+ def default_params
12
+ {}
13
+ end
14
+ end
15
+
16
+ end
data/lib/membership.rb ADDED
@@ -0,0 +1,3 @@
1
+ class Membership < Flareshow::Resource
2
+
3
+ end
data/lib/mime_parts.rb ADDED
@@ -0,0 +1,37 @@
1
+ # Adapted From : http://deftcode.com/code/flickr_upload/multipartpost.rb
2
+
3
+ class Param
4
+ attr_accessor :key, :value, :content_type
5
+ def initialize(key, value, content_type)
6
+ @key = key; @value = value; @content_type = content_type
7
+ end
8
+
9
+ def to_multipart
10
+ return "\r\nContent-Disposition: form-data; name=\"#{CGI::escape(key)}\"\r\n" +
11
+ "Content-Type: #{content_type}; charset=UTF-8" +
12
+ "\r\n\r\n#{value}\r\n"
13
+ end
14
+ end
15
+
16
+ class FileParam
17
+ attr_accessor :key, :filename, :content
18
+ def initialize( key, filename, content )
19
+ @key = key; @filename = filename; @content = content
20
+ end
21
+
22
+ def to_multipart
23
+ return "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{filename}\"\r\n" +
24
+ "Content-Transfer-Encoding: binary\r\n" +
25
+ "Content-type: #{MIME::Types.type_for(filename)}\r\n\r\n" + content + "\r\n"
26
+ end
27
+ end
28
+
29
+ class MultipartPost
30
+ BOUNDARY = "flareshow_multipart_boundary_A0n23hja"
31
+ HEADER = {"Content-type" => "multipart/form-data, boundary=" + BOUNDARY + " "}
32
+
33
+ def self.prepare_query(params)
34
+ query = params.map {|p| "--" + BOUNDARY + "\r\n" + p.to_multipart }.join("") + "--" + BOUNDARY + "--"
35
+ [query, HEADER]
36
+ end
37
+ end
data/lib/post.rb ADDED
@@ -0,0 +1,65 @@
1
+ class Post < Flareshow::Resource
2
+ @attr_accessible = [:content, :flow_id, :files]
3
+ @attr_required = [:flow_id]
4
+
5
+ extend Flareshow::Searchable
6
+
7
+ # permalink to this post
8
+ def permalink(mobile=false)
9
+ if mobile
10
+ "http://#{Flareshow::Service.server.host}/#{Flareshow::Service.server.domain}/shareflow/mobile/post/#{id}"
11
+ else
12
+ "http://#{Flareshow::Service.server.host}/#{Flareshow::Service.server.domain}/shareflow/p/#{id}"
13
+ end
14
+ end
15
+
16
+ # build a new comment but don't immediately persist it
17
+ def build_comment(attributes={})
18
+ c = Comment.new(attributes)
19
+ c.post_id = self.id
20
+ c
21
+ end
22
+
23
+ # create a new comment on the post
24
+ def create_comment(attributes={})
25
+ c = build_comment(attributes)
26
+ c.save
27
+ end
28
+
29
+ # build a new file object on the client but don't commit to the server immediately
30
+ def build_file(file_path)
31
+ self.files ||= []
32
+ self.files += [{"part_id" => "file_#{UUID.generate}", "file_path" => file_path}]
33
+ end
34
+
35
+ # upload a file to a post
36
+ def create_file(file_path)
37
+ self.files = []
38
+ self.build_file(file_path)
39
+ self.save
40
+ self.files = nil
41
+ end
42
+
43
+ # persisted files for this post
44
+ def files
45
+ FileAttachment.find({:post_id => id})
46
+ end
47
+
48
+ # comments for this post
49
+ def comments
50
+ Comment.find({:post_id => id})
51
+ end
52
+
53
+ # user for this post
54
+ def user
55
+ return User.current unless user_id
56
+ User.first({:id => user_id})
57
+ end
58
+
59
+ # get the flow for this post
60
+ def flow
61
+ return false unless flow_id
62
+ Flow.first({:id => flow_id})
63
+ end
64
+
65
+ end
data/lib/resource.rb ADDED
@@ -0,0 +1,205 @@
1
+ class Flareshow::Resource
2
+
3
+ class << self
4
+ attr_accessor :read_only, :attr_accessible, :attr_required
5
+
6
+ def default_params
7
+ # default parameters that are included with query
8
+ # requests unless they are explicitly overridden
9
+ {:order => "created_at desc"}
10
+ end
11
+
12
+ # return the resource key for this resource
13
+ def resource_key
14
+ Flareshow::ClassToResourceMap[self.name]
15
+ end
16
+
17
+ # find an existing instance of this object in the client or create a new one
18
+ def get_from_cache(id)
19
+ store.get_resource(resource_key, id)
20
+ end
21
+
22
+ # list out the instances in memory
23
+ def list_cache
24
+ store.list_resource(resource_key)
25
+ end
26
+
27
+ # store the response resources in the cache
28
+ def cache_response(response)
29
+ Flareshow::CacheManager.assimilate_resources(response["resources"])
30
+ end
31
+
32
+ # find a resource by querying the server
33
+ # store the results in the cache and return
34
+ # the keyed resources for the model performing the query
35
+ def find(params={})
36
+ params = default_params.merge(params)
37
+ response = Flareshow::Service.query({resource_key => params})
38
+ (cache_response(response) || {})[resource_key]
39
+ end
40
+
41
+ # find just one resource matching the conditions specified
42
+ def first(params={})
43
+ params = default_params.merge(params)
44
+ params = params.merge({"limit" => 1})
45
+ response = Flareshow::Service.query({resource_key => params})
46
+ (cache_response(response) || {})[resource_key].first
47
+ end
48
+
49
+ # create a resource local and sync it to the server
50
+ def create(params={})
51
+ new(params).save
52
+ end
53
+
54
+ #
55
+ def store
56
+ Flareshow::CacheManager.cache
57
+ end
58
+ end
59
+
60
+ # constructor
61
+ # build a new Flareshow::Base resource
62
+ def initialize(data={}, source = :client)
63
+ @data = {}
64
+ Flareshow::Util.log_info("creating #{self.class.name} with data from #{source}")
65
+ update(data, source)
66
+ @data["id"] = UUID.generate.upcase if source == :client
67
+ end
68
+
69
+ # return the resource key for this resource
70
+ def resource_key
71
+ Flareshow::ClassToResourceMap[self.class.name]
72
+ end
73
+
74
+ # store a resource in the cache
75
+ def cache
76
+ self.class.store.store.set_resource(resource_key, id, self)
77
+ end
78
+
79
+ # ==============================
80
+ # = Server Persistence Actions =
81
+ # ==============================
82
+
83
+ # reload the resource from the server
84
+ def refresh
85
+ results = self.find({"id" => id})
86
+ mark_destroyed! if results.empty?
87
+ self
88
+ end
89
+
90
+ # save a resource to the server
91
+ def save
92
+ raise Flareshow::APIAccessException if self.class.read_only
93
+ key = Flareshow::ClassToResourceMap[self.class.name]
94
+ raise Flareshow::MissingRequiredField unless !self.class.attr_required || (self.class.attr_required.map{|a|a.to_s} - @data.keys).empty?
95
+ response = Flareshow::Service.commit({resource_key => [(self.changes || {}).merge({"id" => id})] })
96
+ cache_response(response)
97
+ self
98
+ rescue Exception => e
99
+ Flareshow::Util.log_error e.message
100
+ Flareshow::Util.log_error e.backtrace.join("\n")
101
+ throw e
102
+ false
103
+ end
104
+
105
+ # destroy the resource on the server
106
+ def destroy
107
+ raise Flareshow::APIAccessException if self.class.read_only
108
+ response = Flareshow::Service.commit({resource_key => [{"id" => id, "_removed" => true}]})
109
+ cache_response(response)
110
+ mark_destroyed!
111
+ self
112
+ rescue Exception => e
113
+ Flareshow::Util.log_error e.message
114
+ throw e
115
+ false
116
+ end
117
+
118
+ # has this resource been destroyed
119
+ def destroyed?
120
+ self._removed || self.frozen?
121
+ end
122
+
123
+ private
124
+
125
+ # clear the element of the cache
126
+ def mark_destroyed!
127
+ self.freeze
128
+ self._removed=true
129
+ self.class.store.delete_resource(resource_key, id)
130
+ end
131
+
132
+ public
133
+
134
+ # ==================================
135
+ # = Attribute and State Management =
136
+ # ==================================
137
+
138
+ # return the server id of a resource
139
+ def id
140
+ @data["id"]
141
+ end
142
+
143
+ # update the instance data for this resource
144
+ # keeping track of dirty state if the update came from
145
+ # the client
146
+ def update(attributes, source = :client)
147
+ raise Flareshow::APIAccessException if self.class.read_only && source == :client
148
+ attributes.each do |p|
149
+ key, value = p[0], p[1]
150
+ self.set(key, value, source)
151
+ end
152
+ rescue Exception => e
153
+ Flareshow::Util.log_error e.message
154
+ throw e
155
+ false
156
+ end
157
+
158
+ # keep track of dirty state on the client by maintaining a copy
159
+ # of the original state of each intstance variable when it is provided by
160
+ # the server
161
+ def set(key, value, source = :client)
162
+ raise Flareshow::APIAccessException if self.class.read_only && source == :client
163
+ if self.class.attr_accessible &&
164
+ !(/_removed/).match(key.to_s) &&
165
+ !self.class.attr_accessible.include?(key.to_sym) && source == :client
166
+ Flareshow::Util.log_error "#{self.class.name}.#{key} is not a writable field"
167
+ raise Flareshow::APIAccessException
168
+ end
169
+ # Flareshow::Util.log_info("setting #{key} : #{value}")
170
+ @data["original_#{key}"] = value if source == :server
171
+ @data[key.to_s]=value
172
+ rescue Exception => e
173
+ Flareshow::Util.log_error e.message
174
+ throw e
175
+ false
176
+ end
177
+
178
+ # get a data value
179
+ def get(key)
180
+ @data[key]
181
+ end
182
+
183
+ # all the state that has been modified on the client
184
+ def changes
185
+ attributes = @data.inject({}) do |memo, pair|
186
+ key, value = *pair
187
+ if @data[key] != @data["original_#{key}"] && !key.to_s.match(/original/)
188
+ memo[key] = value
189
+ end
190
+ memo
191
+ end
192
+ end
193
+
194
+ # fallback to getter or setter
195
+ def method_missing(meth, *args)
196
+ meth = meth.to_s
197
+ meth.match(/\=/) ? set(meth.gsub(/\=/,''), *args) : get(meth)
198
+ end
199
+
200
+ # has this model been removed on the server
201
+ def method_name
202
+ !!self._removed
203
+ end
204
+
205
+ end
data/lib/searchable.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Flareshow
2
+ module Searchable
3
+
4
+ def search(keywords)
5
+ self.find({:keywords => keywords})
6
+ end
7
+
8
+ end
9
+ end
data/lib/server.rb ADDED
@@ -0,0 +1,2 @@
1
+ # represents a server connection for a Flareshow resource
2
+ class Server < Struct.new(:host, :domain); end
data/lib/service.rb ADDED
@@ -0,0 +1,195 @@
1
+ # provides an interface to the shareflow api
2
+ class Flareshow::Service
3
+
4
+ # =================
5
+ # = Class Methods =
6
+ # =================
7
+ class << self
8
+ attr_accessor :server, :debug_output, :key
9
+
10
+ # setup the service to use a particular host and domain
11
+ def configure(subdomain=nil, host='api.zenbe.com')
12
+ raise Flareshow::ConfigurationException unless subdomain
13
+ self.server=Server.new(host, subdomain)
14
+ @auth_endpoint = @api_endpoint = nil
15
+ end
16
+
17
+ # return the authentication endpoint for a given host and domain
18
+ def auth_endpoint
19
+ @auth_endpoint ||= URI.parse("http://#{server.host}/#{server.domain}/shareflow/api/v2/auth.json")
20
+ end
21
+
22
+ # return the api endpoint for a given host and domain
23
+ def api_endpoint
24
+ @api_endpoint ||= URI.parse("http://#{server.host}/#{server.domain}/shareflow/api/v2.json")
25
+ end
26
+
27
+ # has the server been configured?
28
+ def server_defined?
29
+ !!server
30
+ end
31
+
32
+ # make a post request to an endpoint
33
+ # returns a hash of
34
+ # - status code
35
+ # - headers
36
+ # - body
37
+ def post(uri, params, files=[])
38
+ raise Flareshow::ConfigurationException unless server_defined?
39
+ # create the request object
40
+ req = Net::HTTP::Post.new(uri.path)
41
+ # set request headers
42
+ req.add_field "Accept", "application/json"
43
+ req.add_field "User-Agent", "ruby flareshow"
44
+ req.add_field "Accept-Encoding", "compress, gzip"
45
+
46
+ # just json
47
+ if !files || files.empty?
48
+ req.add_field "Content-type", "application/json; charset=UTF-8"
49
+ # set the postbody
50
+ req.body = params.to_json
51
+ # handle file params
52
+ else
53
+ params = params.map{|p|
54
+ val = p[1].is_a?(String) ? p[1] : p[1].to_json
55
+ Param.new(p[0], val, "application/json")
56
+ }
57
+ files.each do |f|
58
+ params << FileParam.new(f["part_id"], f["file_path"], File.read(f["file_path"]))
59
+ end
60
+ body, header = *MultipartPost.prepare_query(params)
61
+ req.add_field "Content-type", header["Content-type"]
62
+ req.body = body
63
+ end
64
+
65
+ # make the request
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ http.set_debug_output DEFAULT_LOGGER if debug_output
68
+ response = http.start {|h| h.request(req)}
69
+
70
+ # normalize the response
71
+ response = process_response(response)
72
+ log_response(response)
73
+ response
74
+ end
75
+
76
+ # do a get request
77
+ def http_get(uri)
78
+ uri = URI.parse(uri) unless uri.is_a? URI
79
+ req = Net::HTTP::Get.new(uri.path + "?key=#{@key}")
80
+ req.add_field "User-Agent", "ruby flareshow"
81
+ http = Net::HTTP.new(uri.host, uri.port)
82
+ http.set_debug_output DEFAULT_LOGGER if debug_output
83
+ response = http.start{|h|http.request(req)}
84
+ response = process_response(response)
85
+ Flareshow::Util.log_error("resource not found") if response["status_code"] == 404
86
+ response
87
+ end
88
+
89
+ # log the response
90
+ def log_response(response)
91
+ # log a service exception
92
+ case response["status_code"]
93
+ when 400
94
+ log_service_error(response)
95
+ when 500
96
+ log_service_error(response)
97
+ when 403
98
+ log_service_error(response)
99
+ end
100
+ end
101
+
102
+ # authenticate with the server using an http post
103
+ def authenticate(login, password)
104
+ params = {:login => login, :password => password}
105
+ response = post(auth_endpoint, params)
106
+ # store the auth token returned from the authentication request
107
+ if response["status_code"] == 200
108
+ @key = response["resources"]["data"]["auth_token"]
109
+ response
110
+ else
111
+ raise Flareshow::AuthenticationFailed
112
+ end
113
+ rescue Exception => e
114
+ Flareshow::Util.log_error e.message
115
+ Flareshow::Util.log_error e.backtrace.join("\n")
116
+ end
117
+
118
+ # query the server with an http post of the query params
119
+ def query(params={})
120
+ raise Flareshow::AuthenticationRequired unless @key
121
+ params = {"key" => @key, "query" => params}
122
+ post(api_endpoint, params)
123
+ end
124
+
125
+ # commit changes to the server with an http post
126
+ def commit(params={}, files=[])
127
+ raise Flareshow::AuthenticationRequired unless @key
128
+ params, files = *files_from_params(params) if params["posts"]
129
+ params = {"key" => @key, "data" => params}
130
+ post(api_endpoint, params, files)
131
+ end
132
+
133
+ # get the files out of the params
134
+ def files_from_params(params)
135
+ files = []
136
+ # add any file parts passed in and assign a part id to the
137
+ params["posts"] = (params["posts"]).map do |f|
138
+ if f["files"]
139
+ f["files"] = (f["files"]).map do |ff|
140
+ # we only want to send up new files from the client so we'll strip out any existing
141
+ # files in the params that came down from the server
142
+ val = nil
143
+ if ff["part_id"]
144
+ val = {"part_id" => ff["part_id"], "file_path" => ff["file_path"]}
145
+ files << val
146
+ end
147
+ val
148
+ end.compact
149
+ end
150
+ f
151
+ end
152
+ [params, files]
153
+ end
154
+
155
+ # get the interesting bits out of the curl response
156
+ def process_response(response)
157
+ # read the response headers
158
+ headers = {}; response.each_header{|k,v| headers[k] = v}
159
+ # build a response object
160
+ response_obj = {"status_code" => response.code.to_i, "headers" => headers, "body" => response.plain_body}
161
+
162
+ # automatically handle json response
163
+ if (/json/i).match(response.content_type)
164
+ response_obj["resources"] = JSON.parse(response_obj["body"])
165
+ end
166
+
167
+ # log the response
168
+ Flareshow::Util.log_info(response_obj["status_code"])
169
+ Flareshow::Util.log_info(response_obj["body"])
170
+ response_obj
171
+ end
172
+
173
+ # log a service error
174
+ def log_service_error(response)
175
+ if response["resources"]
176
+ Flareshow::Util.log_error(response["resources"]["message"])
177
+ Flareshow::Util.log_error(response["resources"]["details"])
178
+ else
179
+ Flareshow::Util.log_error(response["body"])
180
+ end
181
+ end
182
+
183
+ # clear the authenticated session
184
+ def logout
185
+ @key = nil
186
+ end
187
+
188
+ # are we authenticated
189
+ def authenticated?
190
+ !!@key
191
+ end
192
+
193
+ end
194
+
195
+ end
data/lib/user.rb ADDED
@@ -0,0 +1,60 @@
1
+ class User < Flareshow::Resource
2
+
3
+ @read_only=true
4
+
5
+ # =================
6
+ # = Class Methods =
7
+ # =================
8
+ class << self
9
+
10
+ # return the current authenticated user
11
+ def current
12
+ @current
13
+ end
14
+
15
+ # authenticate user credentials
16
+ def log_in(login, password)
17
+ response = Flareshow::Service.authenticate(login, password)
18
+ user_data = response["resources"]["data"]
19
+ Flareshow::CacheManager.assimilate_resources({resource_key => [user_data]})
20
+ @current = User.get_from_cache(user_data["id"])
21
+ end
22
+
23
+ # ==================
24
+ # = Authentication =
25
+ # ==================
26
+ def logout
27
+ Flareshow::Service.logout
28
+ @current = nil
29
+ end
30
+
31
+ end
32
+
33
+ # ====================
34
+ # = Instance Methods =
35
+ # ====================
36
+
37
+ # ================
38
+ # = Associations =
39
+ # ================
40
+ def flows
41
+ Flow.find({"user_id" => ["in", id]})
42
+ end
43
+
44
+ def posts
45
+ Post.find({"user_id" => ["in", id]})
46
+ end
47
+
48
+ def comments
49
+ Comment.find({"user_id" => ["in", id]})
50
+ end
51
+
52
+ def files
53
+ File.find({"user_id" => ["in", id]})
54
+ end
55
+
56
+ def logged_in?
57
+ @current
58
+ end
59
+
60
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,13 @@
1
+ class Flareshow::Util
2
+
3
+ class << self
4
+ def log_info(message)
5
+ DEFAULT_LOGGER.info(message)
6
+ end
7
+
8
+ def log_error(message)
9
+ DEFAULT_LOGGER.error(message)
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class FlareshowTest < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'flareshow'
8
+
9
+ class Test::Unit::TestCase
10
+ end