kiosk 0.0.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/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +26 -0
- data/lib/kiosk.rb +58 -0
- data/lib/kiosk/bad_config.rb +5 -0
- data/lib/kiosk/cacheable.rb +6 -0
- data/lib/kiosk/cacheable/connection.rb +102 -0
- data/lib/kiosk/cacheable/resource.rb +90 -0
- data/lib/kiosk/cdn.rb +28 -0
- data/lib/kiosk/claim.rb +12 -0
- data/lib/kiosk/claim/node_claim.rb +44 -0
- data/lib/kiosk/claim/path_claim.rb +11 -0
- data/lib/kiosk/claimed_node.rb +9 -0
- data/lib/kiosk/controller.rb +61 -0
- data/lib/kiosk/document.rb +6 -0
- data/lib/kiosk/indexer.rb +5 -0
- data/lib/kiosk/indexer/adapter.rb +14 -0
- data/lib/kiosk/indexer/adapter/base.rb +10 -0
- data/lib/kiosk/indexer/adapter/thinking_sphinx_adapter.rb +266 -0
- data/lib/kiosk/localizable.rb +5 -0
- data/lib/kiosk/localizable/resource.rb +30 -0
- data/lib/kiosk/localizer.rb +17 -0
- data/lib/kiosk/origin.rb +20 -0
- data/lib/kiosk/prospective_node.rb +18 -0
- data/lib/kiosk/prospector.rb +75 -0
- data/lib/kiosk/resource.rb +313 -0
- data/lib/kiosk/resource_error.rb +5 -0
- data/lib/kiosk/resource_not_found.rb +12 -0
- data/lib/kiosk/resource_uri.rb +82 -0
- data/lib/kiosk/rewrite.rb +13 -0
- data/lib/kiosk/rewrite/cdn_rewrite.rb +9 -0
- data/lib/kiosk/rewrite/node_rewrite.rb +18 -0
- data/lib/kiosk/rewrite/path_rewrite.rb +10 -0
- data/lib/kiosk/rewriter.rb +72 -0
- data/lib/kiosk/searchable.rb +5 -0
- data/lib/kiosk/searchable/resource.rb +15 -0
- data/lib/kiosk/tasks/kiosk.rake +6 -0
- data/lib/kiosk/version.rb +3 -0
- metadata +136 -0
@@ -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,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
|