kiosk 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ module Kiosk
2
+ module ProspectiveNode
3
+ # Attempts to match the given pattern against the resource URI.
4
+ #
5
+ def match_uri(pattern, shim_patterns = {})
6
+ resource_uri.match(pattern, shim_patterns)
7
+ rescue URI::BadURIError
8
+ nil
9
+ end
10
+
11
+ # Returns either the nodes +href+ or +src+ attribute as a parsed
12
+ # +ResourceURI+.
13
+ #
14
+ def resource_uri
15
+ ResourceURI.parse(self['href'] || self['src'] || '')
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,75 @@
1
+ require 'active_support/concern'
2
+
3
+ module Kiosk
4
+ module Prospector
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # Tags portions of any resource content as "belonging" to this resource.
9
+ # For instance, a link to a post could belong to a Post resource. This
10
+ # allows for content to be later rewritten in the controller as deemed
11
+ # necessary. A typical scenario for this requirement might be when URIs
12
+ # within the content need to be targetted to the host app and not a CMS
13
+ # with which you are integrating.
14
+ #
15
+ # For example, links to http://cms.example/post/123 could be tagged
16
+ # by a Post resource and later rewritten by a PostsController to be
17
+ # http://app.example/posts/123.
18
+ #
19
+ # Available options:
20
+ #
21
+ # - +:selector+: The CSS selector used to find content nodes.
22
+ # - +:priority+: Claims with higher priority (a lower value) take
23
+ # precedence. Symbols +:high+, +:normal+, +:low+ map to
24
+ # values -9, 0, and 9 respectively.
25
+ # - +:pattern+: For use with +claims_path_content+. Specifies the
26
+ # pattern to use when matching a node's URI. See
27
+ # +ResourceURI#match+ for documentation on pattern syntax.
28
+ #
29
+ # Different types of claims may be supported and are made by calling
30
+ # +claims_<type>_content+. The default is a +Kiosk::Claim::NodeClaim+.
31
+ #
32
+ # Examples:
33
+ #
34
+ # class Attachment
35
+ # claims_content(:selector => 'img.attachment') do |node|
36
+ # (m = node['src'].match(/\d+$)) && { :id => m[1] }
37
+ # end
38
+ # end
39
+ #
40
+ # Typically, you'd want to use the extensions of +ProspectiveNode+.
41
+ #
42
+ # class Post
43
+ # claims_content(:selector => 'a.post') { |node| node.match_uri 'post/:slug' }
44
+ # end
45
+ #
46
+ # A +PathClaim+ is also available that simplified this pattern. See
47
+ # +ResourceURI#match+ for documentation regarding the URI pattern.
48
+ #
49
+ # class Post
50
+ # claims_path_content(:selector => 'a.post', :pattern => 'post/:slug')
51
+ # end
52
+ #
53
+ def claims_content(options = {}, &parser)
54
+ claims_x_content(:node, options, &parser)
55
+ end
56
+
57
+ # Handles calls to +claims_<type>_content+. See +method_missing+.
58
+ #
59
+ def claims_x_content(type, options = {}, &parser)
60
+ Kiosk.rewriter.add_claim(Kiosk::Claim.new(type, self, options, &parser), options)
61
+ end
62
+
63
+ # Implements +claims_<type>_content+ methods. See +claims_x_content+.
64
+ #
65
+ def method_missing(name, *args, &blk)
66
+ case name.to_s
67
+ when /^claims_(\w+)_content$/
68
+ claims_x_content($1.to_sym, *args, &blk)
69
+ else
70
+ super
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,313 @@
1
+ require 'active_resource'
2
+
3
+ # Proxy for content resources.
4
+ #
5
+ module Kiosk
6
+ class Resource < ::ActiveResource::Base
7
+ include Cacheable::Resource
8
+ include Localizable::Resource
9
+ include Prospector
10
+
11
+ ##############################################################################
12
+ # ActiveResource config
13
+ ##############################################################################
14
+ self.site = Kiosk.origin.site
15
+ self.format = :json
16
+
17
+ schema do
18
+ attribute 'slug', :string
19
+ end
20
+
21
+ ##############################################################################
22
+ # Caching
23
+ ##############################################################################
24
+ cached_expire_in { |resource| resource['status'] == 'error' ? 30.minutes : 1.day }
25
+
26
+ ##############################################################################
27
+ # Class methods
28
+ ##############################################################################
29
+ class << self
30
+ # Returns all instances of the resource.
31
+ #
32
+ def all
33
+ find(:all)
34
+ end
35
+
36
+ # Reimplements the +ActiveResource+ path constructor to work with the
37
+ # WordPress JSON-API plugin.
38
+ #
39
+ def element_path(id, prefix_options = {}, query_options = nil)
40
+ "#{api_path_to("get_#{element_name}")}#{query_string({:id => id}.merge(query_options || {}))}"
41
+ end
42
+
43
+ def element_path_by_slug(slug, prefix_options = {}, query_options = nil)
44
+ "#{api_path_to("get_#{element_name}")}#{query_string({:slug => slug}.merge(query_options || {}))}"
45
+ end
46
+
47
+ # Adds functionality to the +ActiveResource.find+ method to allow for
48
+ # specifying the WordPress JSON API method that should be used. This
49
+ # simplifies definition of scopes in derived models.
50
+ #
51
+ def find(*arguments)
52
+ scope = arguments.slice!(0)
53
+ options = arguments.slice!(0) || {}
54
+
55
+ if options.key?(:method)
56
+ options[:from] = api_path_to(options[:method])
57
+ options.delete(:method)
58
+ end
59
+
60
+ super(scope, options)
61
+ end
62
+
63
+ # Finds all resources by the given related resource.
64
+ #
65
+ # Example:
66
+ #
67
+ # Post.find_by_associated(category)
68
+ #
69
+ # Is the same as invoking:
70
+ #
71
+ # Post.find(:all, :method => "get_category_posts", :params => {:id => category.id})
72
+ #
73
+ def find_by_associated(resource, params = {})
74
+ find(:all,
75
+ :method => "get_#{resource.class.element_name}_#{element_name.pluralize}",
76
+ :params => params.merge({:id => resource.id}))
77
+ end
78
+
79
+ # Finds the resource by the given slug.
80
+ #
81
+ def find_by_slug(slug)
82
+ find(:one, :method => "get_#{element_name}", :params => {:slug => slug})
83
+ end
84
+
85
+ # Reimplements the +ActiveResource+ path constructor to work with the
86
+ # WordPress JSON-API plugin.
87
+ #
88
+ def collection_path(prefix_options = {}, query_options = nil)
89
+ "#{api_path_to("get_#{element_name}_index")}#{query_string(query_options)}"
90
+ end
91
+
92
+ # Reimplements the +ActiveResource+ method to check for bad responses
93
+ # before instantiating a collection.
94
+ #
95
+ def instantiate_collection(collection, prefix_options = {})
96
+ super(normalize_response(collection, true), prefix_options)
97
+ end
98
+
99
+ # Reimplements the +ActiveResource+ method to check for bad responses
100
+ # before instantiating an object.
101
+ #
102
+ def instantiate_record(record, prefix_options = {})
103
+ super(normalize_response(record), prefix_options)
104
+ end
105
+
106
+ # Executes the given block within a scope where all requests for this
107
+ # content resource are appended with the given parameters.
108
+ #
109
+ # class Post < Resource; end
110
+ #
111
+ # Post.with_parameters(:language => 'en') do
112
+ # english_posts = Post.find(:all)
113
+ # english_pages = Page.find(:all)
114
+ # end
115
+ #
116
+ # Scopes can be nested.
117
+ #
118
+ # Post.with_parameters(:language => 'es') do
119
+ # Post.with_parameters(:recent => true) do
120
+ # recent_spanish_posts = Post.find(:all)
121
+ # end
122
+ # end
123
+ #
124
+ # Scopes are inherited.
125
+ #
126
+ # Resource.with_parameters(:language => 'fr') do
127
+ # french_posts = Post.find(:all)
128
+ # end
129
+ #
130
+ # However, nesting is still respected.
131
+ #
132
+ # Resource.with_parameters(:language => 'fr') do
133
+ # Post.with_parameters(:language => 'en') do
134
+ # english_posts = Post.find(:all)
135
+ # end
136
+ # end
137
+ #
138
+ # Even with this nesting inverted.
139
+ #
140
+ # Post.with_parameters(:language => 'fr') do
141
+ # Resource.with_parameters(:language => 'en') do
142
+ # english_posts = Post.find(:all)
143
+ # end
144
+ # end
145
+ #
146
+ def with_parameters(params = {})
147
+ push_to_query_scope_stack(params)
148
+
149
+ begin
150
+ yield
151
+ ensure
152
+ pop_from_query_scope_stack
153
+ end
154
+ end
155
+
156
+ protected
157
+
158
+ # Returns the path to the given method of the WordPress API.
159
+ #
160
+ def api_path_to(method)
161
+ "#{site.path}api/#{method}/"
162
+ end
163
+
164
+ # Checks the given response for errors and normalizes its structure. A
165
+ # response from the API includes an envelope, which must be checked for
166
+ # the response status ("ok" or "error"). If an error is found, an
167
+ # exception is raised.
168
+ #
169
+ def normalize_response(response, collection = false)
170
+ response = case response['status']
171
+ when 'ok'
172
+ response[collection ? element_name.pluralize : element_name]
173
+ when 'error'
174
+ raise_error(response['error'])
175
+ else
176
+ # This isn't a response envelope. Just let it pass through.
177
+ response
178
+ end
179
+
180
+ camelcase_keys(response)
181
+ end
182
+
183
+ # Reimplements the parent method to include parameters of the current
184
+ # query scope. See +with_parameters+.
185
+ #
186
+ def query_string(options)
187
+ scoped_options = my_query_scope_stack.inject({}) do |scoped_options,(klass,opt_stack)|
188
+ if self.ancestors.include?(klass)
189
+ scoped_options = opt_stack.reduce(scoped_options) do |scoped_options,opts|
190
+ scoped_options.merge(opts)
191
+ end
192
+ else
193
+ scoped_options
194
+ end
195
+ end
196
+
197
+ super(scoped_options.merge(options))
198
+ end
199
+
200
+ # Handles errors returned by the WordPress JSON API.
201
+ #
202
+ def raise_error(error)
203
+ case error
204
+ when 'Not found.'
205
+ raise Kiosk::ResourceNotFound.new(error)
206
+ when /Un?known method '(\w+)'/ # note the possibility of a spelling error
207
+ raise NotImplementedError.new(error)
208
+ else
209
+ raise Kiosk::ResourceError.new(error)
210
+ end
211
+ end
212
+
213
+ private
214
+
215
+ mattr_accessor :query_scope_stack
216
+
217
+ # Filters and sorts the query-scope stack so that only scopes relevant
218
+ # to this class are applied and are applied in order from furthest
219
+ # ancestor to nearest.
220
+ #
221
+ def my_query_scope_stack
222
+ if query_scope_stack
223
+ Hash[query_scope_stack.select do |klass,stack|
224
+ self.ancestors.include?(klass)
225
+ end.sort_by do |klass,stack|
226
+ self.ancestors.index(klass) * -1
227
+ end]
228
+ else
229
+ {}
230
+ end
231
+ end
232
+
233
+ def push_to_query_scope_stack(params)
234
+ self.query_scope_stack ||= {}
235
+ self.query_scope_stack[self] ||= []
236
+
237
+ # Append stacks for this class and all descendent classes.
238
+ query_scope_stack.each_key do |klass|
239
+ query_scope_stack[klass].push(params) if klass.ancestors.include?(self)
240
+ end
241
+ end
242
+
243
+ def pop_from_query_scope_stack
244
+ # Pop stacks for this class and all descendent classes.
245
+ query_scope_stack.each_key do |klass|
246
+ query_scope_stack[klass].pop if klass.ancestors.include?(self)
247
+ end
248
+ end
249
+
250
+ # Recursively changes the keys of the given hash (or array of hashes)
251
+ # to camelcase.
252
+ #
253
+ def camelcase_keys(obj)
254
+ case obj
255
+ when Hash
256
+ obj.inject({}) { |hash,(k,v)| hash[k.to_s.underscore] = camelcase_keys(v); hash }
257
+ when Array
258
+ obj.map { |v| camelcase_keys(v) }
259
+ else
260
+ obj
261
+ end
262
+ end
263
+ end
264
+
265
+ ##############################################################################
266
+ # Instance methods
267
+ ##############################################################################
268
+
269
+ # Returns the rewritten resource content. See +raw_content+ for untouched
270
+ # content.
271
+ #
272
+ def content
273
+ @content ||= raw_content && Kiosk.rewriter.rewrite(raw_content)
274
+ end
275
+
276
+ # Returns the rewritten resource excerpt. See +raw_excerpt+ for untouched
277
+ # content.
278
+ #
279
+ def excerpt
280
+ @excerpt ||= raw_excerpt && Kiosk.rewriter.rewrite(raw_excerpt)
281
+ end
282
+
283
+ # Destroying is not supported.
284
+ #
285
+ def destroy
286
+ raise NotImplementedError
287
+ end
288
+
289
+ # Returns the resource content, untouched by the content rewriter.
290
+ #
291
+ def raw_content
292
+ attributes[:content]
293
+ end
294
+
295
+ # Returns the resource excerpt, untouched by the content rewriter.
296
+ #
297
+ def raw_excerpt
298
+ attributes[:excerpt]
299
+ end
300
+
301
+ # Saving is not supported.
302
+ #
303
+ def save
304
+ raise NotImplementedError
305
+ end
306
+
307
+ # Returns the value used in constructing a URL to this object.
308
+ #
309
+ def to_param
310
+ attributes[:slug] || attributes[:id]
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,5 @@
1
+ require 'active_resource'
2
+ require 'active_resource/exceptions'
3
+
4
+ class Kiosk::ResourceError < ActiveResource::ServerError #:nodoc:
5
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_resource'
2
+ require 'active_resource/exceptions'
3
+
4
+ class Kiosk::ResourceNotFound < ActiveResource::ResourceNotFound #:nodoc:
5
+ def initialize(message)
6
+ super(404, message)
7
+ end
8
+
9
+ def to_s
10
+ @message
11
+ end
12
+ end
@@ -0,0 +1,82 @@
1
+ module Kiosk
2
+ module ResourceURI
3
+ DEFAULT_SHIM_PATTERN = /[^\/\?]+/
4
+
5
+ class << self
6
+ def parse(uri_string)
7
+ (uri = URI.parse(uri_string)) && uri.extend(InstanceMethods)
8
+ end
9
+ end
10
+
11
+ module InstanceMethods
12
+ # Matches the relative part of the URI path with the given pattern and
13
+ # returns a hash of parsed attributes constructed from the match. The
14
+ # pattern for each shim can be provided as a hash.
15
+ #
16
+ # Examples:
17
+ #
18
+ # uri = ResourceURI.parse('http://some.example/site/post/some-slug')
19
+ #
20
+ # uri.match('post/:slug')
21
+ # # => { :slug => 'some-slug' }
22
+ #
23
+ # uri.match('post/:id', :id => /\d+/)
24
+ # # => nil
25
+ #
26
+ # Tokens in the pattern starting with '!' are not captured. They can be
27
+ # used to represent portions of the path by name that are purely for
28
+ # readability.
29
+ #
30
+ def match(pattern, shim_patterns = nil)
31
+ shims = []
32
+ shim_patterns ||= {}
33
+
34
+ re_pattern = pattern.gsub(/!(\w+)/, DEFAULT_SHIM_PATTERN.to_s)
35
+ re_pattern = re_pattern.gsub(/:(\w+)/) do |s|
36
+ shims << (shim = $1.to_sym)
37
+ '(' + (shim_patterns[shim] || DEFAULT_SHIM_PATTERN).to_s + ')'
38
+ end
39
+
40
+ re = Regexp.new('^' + re_pattern + '.*$')
41
+
42
+ # Match against the part of the URI path that follows the content origin
43
+ # site path. If the resulting route contains no host and is relative,
44
+ # the URI is within our content origin.
45
+ if route = route_from(Kiosk.origin.site)
46
+ if route.host.nil? and route.relative?
47
+ route.path.match(re) do |matches|
48
+ attributes = {}
49
+ shims.each_with_index { |shim,i| attributes[shim] = matches[i + 1] }
50
+ attributes
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ # Reimplements parent method so as to further qualify routes that are
57
+ # relative but outside the base origin site path (routes that resolve to
58
+ # '../some/external/path').
59
+ #
60
+ def route_from(uri)
61
+ uri = URI.parse(uri) unless uri.is_a?(URI::Generic)
62
+
63
+ # allow a route from http to https to be relative if the host is the
64
+ # same
65
+ if (uri.host == self.host) and (uri.scheme == 'http' and self.scheme == 'https')
66
+ self.scheme = 'http'
67
+ self.port = uri.port
68
+ end
69
+
70
+ route = super(uri)
71
+
72
+ if route.relative? && route.path['../']
73
+ new_uri = uri.clone
74
+ new_uri.path = File.expand_path(route.path, uri.path)
75
+ new_uri
76
+ else
77
+ route
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end