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