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
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
|