kiosk 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Daniel Duvall
|
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,25 @@
|
|
1
|
+
= Kiosk
|
2
|
+
|
3
|
+
Kiosk provides APIs for integrating WordPress content into a Ruby
|
4
|
+
application: a base REST model for retrieving content, a caching layer,
|
5
|
+
and a rewriting engine for canonicalizing and contextualizing content
|
6
|
+
elements.
|
7
|
+
|
8
|
+
This gem was initially developed by the {Office of Letters and
|
9
|
+
Light}[http://lettersandlight.org] for use with {National Novel Writing
|
10
|
+
Month}[http://nanowrimo.org]. It has since been released under the MIT
|
11
|
+
license.
|
12
|
+
|
13
|
+
== Basic Usage Patterns
|
14
|
+
|
15
|
+
(Documentation is forthcoming.)
|
16
|
+
|
17
|
+
== Configuration
|
18
|
+
|
19
|
+
(Documentation is forthcoming.)
|
20
|
+
|
21
|
+
== Roadmap
|
22
|
+
|
23
|
+
In its current state, Kiosk depends on Rails for caching and ActiveResource as
|
24
|
+
a base REST implementation. These dependencies will be made optional in the
|
25
|
+
near future and alternative implementations made possible.
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'rubygems'
|
3
|
+
begin
|
4
|
+
require 'bundler/setup'
|
5
|
+
rescue LoadError
|
6
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rake'
|
10
|
+
|
11
|
+
require 'rspec/core/rake_task'
|
12
|
+
require 'rdoc/task'
|
13
|
+
|
14
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
15
|
+
t.pattern = './spec/**/*_spec.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
19
|
+
rdoc.rdoc_dir = 'rdoc'
|
20
|
+
rdoc.title = 'Kiosk'
|
21
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
22
|
+
rdoc.rdoc_files.include('README.rdoc')
|
23
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
24
|
+
end
|
25
|
+
|
26
|
+
task :default => :spec
|
data/lib/kiosk.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
# Kiosk provides APIs for integrating WordPress content into a Ruby
|
4
|
+
# application: a base REST model for retrieving content, a caching layer, and
|
5
|
+
# a rewriting engine for canonicalizing and contextualizing content elements.
|
6
|
+
#
|
7
|
+
module Kiosk
|
8
|
+
autoload :BadConfig, 'kiosk/bad_config'
|
9
|
+
autoload :ResourceError, 'kiosk/resource_error'
|
10
|
+
autoload :ResourceNotFound, 'kiosk/resource_not_found'
|
11
|
+
|
12
|
+
autoload :Cacheable, 'kiosk/cacheable'
|
13
|
+
autoload :Cdn, 'kiosk/cdn'
|
14
|
+
autoload :Claim, 'kiosk/claim'
|
15
|
+
autoload :ClaimedNode, 'kiosk/claimed_node'
|
16
|
+
autoload :Controller, 'kiosk/controller'
|
17
|
+
autoload :Document, 'kiosk/document'
|
18
|
+
autoload :Indexer, 'kiosk/indexer'
|
19
|
+
autoload :Localizable, 'kiosk/localizable'
|
20
|
+
autoload :Origin, 'kiosk/origin'
|
21
|
+
autoload :ProspectiveNode, 'kiosk/prospective_node'
|
22
|
+
autoload :Prospector, 'kiosk/prospector'
|
23
|
+
autoload :Resource, 'kiosk/resource'
|
24
|
+
autoload :ResourceURI, 'kiosk/resource_uri'
|
25
|
+
autoload :Rewrite, 'kiosk/rewrite'
|
26
|
+
autoload :Rewriter, 'kiosk/rewriter'
|
27
|
+
autoload :Searchable, 'kiosk/searchable'
|
28
|
+
|
29
|
+
##############################################################################
|
30
|
+
# Module methods
|
31
|
+
##############################################################################
|
32
|
+
class << self
|
33
|
+
# Returns the parsed `config/kiosk.yml`.
|
34
|
+
#
|
35
|
+
def config
|
36
|
+
@config ||= YAML.load(File.open("#{Rails.root}/config/kiosk.yml"))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the configuration for the current environment's content origin.
|
40
|
+
#
|
41
|
+
def origin(env = Rails.env)
|
42
|
+
@origins ||= {}
|
43
|
+
|
44
|
+
unless config['origins'] && (config['origins'][env] || config['origins']['default'])
|
45
|
+
raise BadConfig, "no origin configured for the `#{env}' or default environment"
|
46
|
+
end
|
47
|
+
|
48
|
+
@origins[env] ||= Origin.new(config['origins'][env] || config['origins']['default'])
|
49
|
+
end
|
50
|
+
|
51
|
+
# Rewriter object responsible for rewriting resource content.
|
52
|
+
#
|
53
|
+
def rewriter
|
54
|
+
@rewriter ||= Rewriter.new
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module Kiosk
|
2
|
+
module Cacheable::Connection
|
3
|
+
# Sets a value or block used to determine the expiry of written cache
|
4
|
+
# entries. If a block is given, its first argument will be the object that
|
5
|
+
# is about to be written.
|
6
|
+
#
|
7
|
+
def cache_expiry=(expiry)
|
8
|
+
@cache_expiry = expiry.is_a?(Proc) ? expiry : Proc.new { |r| expiry }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the expiry in seconds for the given resource.
|
12
|
+
#
|
13
|
+
def cache_expiry_of(resource)
|
14
|
+
@cache_expiry && @cache_expiry.call(resource)
|
15
|
+
end
|
16
|
+
|
17
|
+
def cache_expire_by_path(path)
|
18
|
+
cache(:delete, cache_key(path))
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_expire_by_pattern(pattern)
|
22
|
+
key_base = cache_key("")
|
23
|
+
key = pattern.is_a?(Regexp) ? /^#{key_base}#{pattern}/ : "#{key_base}#{pattern}"
|
24
|
+
cache(:delete_matched, key)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the type of key matcher supported by the cache store. Note that
|
28
|
+
# the type returned here may not be accurate for cache stores that don't
|
29
|
+
# support matchers at all. For those cases, you should still expect
|
30
|
+
# NotImplemented exceptions to be thrown when calling
|
31
|
+
# +cache_expire_by_pattern+.
|
32
|
+
#
|
33
|
+
def cache_key_matcher
|
34
|
+
case Rails.cache
|
35
|
+
when ActiveSupport::Cache::RedisStore
|
36
|
+
:glob
|
37
|
+
else
|
38
|
+
:regexp
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def get(*arguments) #:nodoc:
|
43
|
+
cache_read_write(arguments.first) { super }
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(*arguments) #:nodoc:
|
47
|
+
cache_expire(arguments.first) { super }
|
48
|
+
end
|
49
|
+
|
50
|
+
def put(*arguments) #:nodoc:
|
51
|
+
cache_expire(arguments.first) { super }
|
52
|
+
end
|
53
|
+
|
54
|
+
def post(*arguments) #:nodoc:
|
55
|
+
cache_expire(arguments.first) { super }
|
56
|
+
end
|
57
|
+
|
58
|
+
def head(*arguments) #:nodoc:
|
59
|
+
cache_read_write(arguments.first) { super }
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Proxy for the Rails cache store.
|
65
|
+
#
|
66
|
+
def cache(operation, *arguments)
|
67
|
+
Rails.cache.send(operation, *arguments) if Rails.cache
|
68
|
+
end
|
69
|
+
|
70
|
+
# Wraps the given block in a cache read/write pattern. If a cached entry
|
71
|
+
# is not found using the given key then the result of the yielded block is
|
72
|
+
# written to the cache using the same key. Either a previously cached or
|
73
|
+
# fresh result is returned.
|
74
|
+
#
|
75
|
+
def cache_read_write(key)
|
76
|
+
if result = cache(:read, cache_key(key))
|
77
|
+
result = JSON.parse(result)
|
78
|
+
elsif result = yield
|
79
|
+
options = (expiry = cache_expiry_of(result)) ? {:expires_in => expiry} : {}
|
80
|
+
cache(:write, cache_key(key), result.to_json, options) if result
|
81
|
+
end
|
82
|
+
result
|
83
|
+
end
|
84
|
+
|
85
|
+
# Wraps the given block in a cache deletion. The cache entry identified by
|
86
|
+
# the given key is deleted after the block is yielded. If any uncaught
|
87
|
+
# exception occurs during the yield, the entry will not be deleted.
|
88
|
+
#
|
89
|
+
def cache_expire(key)
|
90
|
+
result = yield
|
91
|
+
cache(:delete, cache_key(key))
|
92
|
+
result
|
93
|
+
end
|
94
|
+
|
95
|
+
# Constructs a fully qualified URL from the given path, to be used as a
|
96
|
+
# cache key.
|
97
|
+
#
|
98
|
+
def cache_key(path)
|
99
|
+
"#{site.scheme}://#{site.host}:#{site.port}#{path}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Kiosk
|
4
|
+
module Cacheable::Resource
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Specifies the length of time for which a resource should stay cached.
|
9
|
+
# Either a +Fixnum+ (time in seconds) or block can be given. The block
|
10
|
+
# should accept the resource as its argument and should return the
|
11
|
+
# expiry time for the resource in seconds.
|
12
|
+
#
|
13
|
+
# Keep in mind that the underlying cache store may not support
|
14
|
+
# expiration length. In that case, this option has no effect.
|
15
|
+
#
|
16
|
+
def cached_expire_in(expiry = nil, &blk)
|
17
|
+
connection.cache_expiry = expiry || blk
|
18
|
+
end
|
19
|
+
|
20
|
+
# Reimplements method to provide a cacheable connection.
|
21
|
+
#
|
22
|
+
def connection(*args)
|
23
|
+
connection = super(*args)
|
24
|
+
connection.extend(Cacheable::Connection) unless connection.is_a?(Cacheable::Connection)
|
25
|
+
connection
|
26
|
+
end
|
27
|
+
|
28
|
+
# Expire from the cache the resource identified by the given id.
|
29
|
+
#
|
30
|
+
def expire(id)
|
31
|
+
connection.cache_expire_by_path(element_path(id))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Expire from the cache the resource identified by the given slug.
|
35
|
+
#
|
36
|
+
def expire_by_slug(slug)
|
37
|
+
connection.cache_expire_by_path(element_path_by_slug(slug))
|
38
|
+
end
|
39
|
+
|
40
|
+
# Expire from the cache the resource identified by both the slug and id.
|
41
|
+
# Notify any observers of expiration.
|
42
|
+
#
|
43
|
+
def expire_resource(resource)
|
44
|
+
notify_observers(:before_expire, resource)
|
45
|
+
|
46
|
+
expire(resource.id)
|
47
|
+
expire_by_slug(resource.slug)
|
48
|
+
|
49
|
+
if @connection_keys_to_expire
|
50
|
+
begin
|
51
|
+
@connection_keys_to_expire.each { |key| connection.cache_expire_by_pattern(key) }
|
52
|
+
rescue NotImplementedError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
notify_observers(:after_expire, resource)
|
57
|
+
end
|
58
|
+
|
59
|
+
# When a resource is explicitly expired from the cache, cache entries
|
60
|
+
# matching URLs to the given API method are deleted as well.
|
61
|
+
#
|
62
|
+
def expires_connection_methods(*methods)
|
63
|
+
matchers = methods.map do |method|
|
64
|
+
case connection.cache_key_matcher
|
65
|
+
when :glob
|
66
|
+
"#{api_path_to(method)}*"
|
67
|
+
when :regexp
|
68
|
+
/^#{Regexp.escape(api_path_to(method))}/
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
expires_connection_keys(*matchers)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def expires_connection_keys(*keys)
|
78
|
+
(@connection_keys_to_expire ||= []).concat(keys)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
module InstanceMethods
|
83
|
+
# Expire the resource from the cache.
|
84
|
+
#
|
85
|
+
def expire
|
86
|
+
self.class.expire_resource(self)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/kiosk/cdn.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Kiosk
|
2
|
+
class Cdn
|
3
|
+
attr_reader :host
|
4
|
+
|
5
|
+
def initialize(config = {})
|
6
|
+
@host = config['host']
|
7
|
+
end
|
8
|
+
|
9
|
+
def configured?
|
10
|
+
@host
|
11
|
+
end
|
12
|
+
|
13
|
+
def rewrite_node(node)
|
14
|
+
if configured?
|
15
|
+
node.uri_attribute.content = rewrite_uri(URI.parse(node.uri_attribute.content)).to_s
|
16
|
+
end
|
17
|
+
rescue URI::InvalidURIError
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def rewrite_uri(uri)
|
22
|
+
if configured?
|
23
|
+
uri.host = host
|
24
|
+
end
|
25
|
+
uri
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/kiosk/claim.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
module Kiosk
|
2
|
+
module Claim
|
3
|
+
autoload :NodeClaim, 'kiosk/claim/node_claim'
|
4
|
+
autoload :PathClaim, 'kiosk/claim/path_claim'
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def new(type = :node, *args, &blk)
|
8
|
+
"kiosk/claim/#{type}_claim".classify.constantize.new(*args, &blk)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Kiosk
|
2
|
+
module Claim
|
3
|
+
class NodeClaim
|
4
|
+
attr_reader :model, :selector, :parser
|
5
|
+
|
6
|
+
def initialize(model, options = {}, &parser)
|
7
|
+
raise ArgumentError.new('no selector given') unless options[:selector]
|
8
|
+
raise ArgumentError.new('no block provided') unless block_given?
|
9
|
+
|
10
|
+
@model = model
|
11
|
+
@selector = options[:selector]
|
12
|
+
@parser = parser
|
13
|
+
end
|
14
|
+
|
15
|
+
# Stakes the claim over the given content document and yields the provided
|
16
|
+
# block for each match. The block is passed each node, which has been
|
17
|
+
# extended with implementation in +ClaimedNode+.
|
18
|
+
#
|
19
|
+
def stake!(document)
|
20
|
+
select_from(document).each do |node|
|
21
|
+
unless node.is_a?(ClaimedNode)
|
22
|
+
node.extend(ProspectiveNode) unless node.is_a?(ProspectiveNode)
|
23
|
+
|
24
|
+
# If the parser finds anything in the selected node, stake the claim
|
25
|
+
if attributes = parser.call(node)
|
26
|
+
node.extend(ClaimedNode)
|
27
|
+
node.resource = @model.new(attributes)
|
28
|
+
|
29
|
+
yield node if block_given?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
# Implements the selection of nodes to process when staking the claim.
|
38
|
+
#
|
39
|
+
def select_from(document)
|
40
|
+
document.css(selector)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Kiosk
|
2
|
+
module Claim
|
3
|
+
class PathClaim < NodeClaim
|
4
|
+
def initialize(type, options = {}, &parser)
|
5
|
+
raise ArgumentError.new('no path pattern given') unless options[:pattern]
|
6
|
+
|
7
|
+
super(type, options) { |node| node.match_uri(options[:pattern], options[:shims]) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|