kiosk 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
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,5 @@
1
+ require 'active_resource'
2
+ require 'active_resource/exceptions'
3
+
4
+ class Kiosk::BadConfig < StandardError #:nodoc:
5
+ end
@@ -0,0 +1,6 @@
1
+ module Kiosk
2
+ module Cacheable
3
+ autoload :Resource, 'kiosk/cacheable/resource'
4
+ autoload :Connection, 'kiosk/cacheable/connection'
5
+ end
6
+ 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
@@ -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