pebblebed 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
6
+ tmp/
7
+ coverage
8
+ .powder
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in pebblebed.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,21 @@
1
+ require 'deepstruct'
2
+
3
+ module Pebblebed
4
+ class AbstractClient
5
+ def perform(method, url = '', params = {}, &block)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def get(*args, &block)
10
+ perform(:get, *args, &block)
11
+ end
12
+
13
+ def post(*args, &block)
14
+ perform(:post, *args, &block)
15
+ end
16
+
17
+ def delete(*args, &block)
18
+ perform(:delete, *args, &block)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ module Pebblebed
2
+ class CheckpointClient < Pebblebed::GenericClient
3
+ def me
4
+ return @identity if @identity_checked
5
+ @identity_checked = true
6
+ @identity = get("/identities/me")[:identity]
7
+ end
8
+
9
+ def cache_key_for_identity_id(id)
10
+ "identity:#{id}"
11
+ end
12
+
13
+ # Given a list of identity IDs it returns each identity or an empty hash for identities that doesnt exists.
14
+ # If pebbles are configured with memcached, results will be cached.
15
+ # Params: ids a list of identities
16
+ def find_identities(ids)
17
+
18
+ result = {}
19
+ uncached = ids
20
+
21
+ if Pebblebed.memcached
22
+ cache_keys = ids.collect {|id| cache_key_for_identity_id(id) }
23
+ result = Hash[Pebblebed.memcached.get_multi(*cache_keys).map do |key, identity|
24
+ /identity:(?<id>\d+)/ =~ key # yup, this is ugly, but an easy hack to get the actual identity id we are trying to retrieve
25
+ [id.to_i, identity]
26
+ end]
27
+ uncached = ids-result.keys
28
+ end
29
+
30
+ if uncached.size > 0
31
+ request = get("/identities/#{uncached.join(',')},")
32
+ uncached.each_with_index do |id, i|
33
+ identity = request.identities[i].identity.unwrap
34
+ result[id] = identity
35
+ Pebblebed.memcached.set(cache_key_for_identity_id(id), identity, ttl=60*15) if Pebblebed.memcached
36
+ end
37
+ end
38
+ return DeepStruct.wrap(ids.collect {|id| result[id]})
39
+ end
40
+
41
+ def god?
42
+ me.god if me
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,33 @@
1
+ require 'deepstruct'
2
+
3
+ module Pebblebed
4
+ class GenericClient < AbstractClient
5
+ def initialize(session_key, root_url)
6
+ @root_url = root_url
7
+ @root_url = URI(@root_url) unless @root_url.is_a?(URI::HTTP)
8
+ @session_key = session_key
9
+ end
10
+
11
+ def perform(method, url = '', params = {}, &block)
12
+ begin
13
+ result = Pebblebed::Http.send(method, service_url(url), service_params(params), &block)
14
+ return DeepStruct.wrap(Yajl::Parser.parse(result.body))
15
+ rescue Yajl::ParseError
16
+ return result.body
17
+ end
18
+ end
19
+
20
+ def service_url(url)
21
+ result = @root_url.dup
22
+ result.path = result.path.sub(/\/+$/, "") + url
23
+ result
24
+ end
25
+
26
+ def service_params(params)
27
+ params ||= {}
28
+ params['session'] = @session_key if @session_key
29
+ params
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ require 'deepstruct'
2
+ require 'futurevalue'
3
+
4
+ # A client that talks to a number of clients all at on
5
+ module Pebblebed
6
+ class QuorumClient < AbstractClient
7
+ def initialize(services, session_key)
8
+ @clients = Hash[services.map do |service|
9
+ [service, Pebblebed::GenericClient.new(session_key, Pebblebed.root_url_for(service))]
10
+ end]
11
+ end
12
+
13
+ def perform(method, url = '', params = {}, &block)
14
+ # Using Future::Value perform the full quorum in parallel
15
+ results = @clients.map do |service, client|
16
+ response = [service]
17
+ response << Future::Value.new do
18
+ begin
19
+ client.perform(method, url, params, &block)
20
+ rescue HttpError => e
21
+ e
22
+ end
23
+ end
24
+ response
25
+ end
26
+ # Unwrap future values and thereby joining all threads
27
+ Hash[results.map{|service, response| [service, response.value]}]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,57 @@
1
+ module Pebblebed
2
+ class Builder
3
+ def host(value)
4
+ Pebblebed.host = value
5
+ end
6
+
7
+ def memcached(value)
8
+ Pebblebed.memcached = value
9
+ end
10
+
11
+ def service(name, options = {})
12
+ Pebblebed.require_service(name, options)
13
+ end
14
+ end
15
+
16
+ def self.config(&block)
17
+ Builder.new.send(:instance_eval, &block)
18
+ end
19
+
20
+ def self.require_service(name, options = {})
21
+ (@services ||= {})[name.to_sym] = options
22
+ Pebblebed::Connector.class_eval <<-END
23
+ def #{name}
24
+ self["#{name}"]
25
+ end
26
+ END
27
+ end
28
+
29
+ def self.host
30
+ @host
31
+ end
32
+
33
+ def self.host=(value)
34
+ @host = value
35
+ end
36
+
37
+ def self.memcached
38
+ @memcached
39
+ end
40
+
41
+ def self.memcached=(value)
42
+ @memcached = value
43
+ end
44
+
45
+ def self.services
46
+ @services.keys
47
+ end
48
+
49
+ def self.version_of(service)
50
+ return 1 unless @services && @services[service.to_sym]
51
+ @services[service.to_sym][:version] || 1
52
+ end
53
+
54
+ def self.root_url_for(service, url_opts={})
55
+ URI("http://#{url_opts[:host] || self.host}/api/#{service}/v#{version_of(service)}/")
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ require 'active_support/inflector'
2
+
3
+ module Pebblebed
4
+ class Connector
5
+ def initialize(key = nil, url_opts = {})
6
+ @key = key
7
+ @clients = {}
8
+ @url_opts = url_opts
9
+ end
10
+
11
+ def [](service)
12
+ client_class = self.class.client_class_for(service)
13
+ (@clients[service.to_sym] ||= client_class.new(@key, Pebblebed.root_url_for(service.to_s, @url_opts)))
14
+ end
15
+
16
+ # Returns a quorum client that talks to the provided list of
17
+ # pebbles all at once. The result is a hash of services and their
18
+ # responses. If any service returned an error, their entry
19
+ # in the hash will be an HttpError object.
20
+ def quorum(services = nil, session_key = nil)
21
+ QuorumClient.new(services || Pebblebed.services, session_key)
22
+ end
23
+
24
+
25
+ def parts
26
+ @parts ||= Pebblebed::Parts.new(self)
27
+ end
28
+
29
+ def self.client_class_for(service)
30
+ class_name = ActiveSupport::Inflector.classify(service)+'Client'
31
+ begin
32
+ Pebblebed.const_get(class_name)
33
+ rescue NameError
34
+ GenericClient
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,98 @@
1
+ # A wrapper for all low level http client stuff
2
+
3
+ require 'uri'
4
+ require 'curl'
5
+ require 'yajl'
6
+ require 'queryparams'
7
+ require 'nokogiri'
8
+ require 'pathbuilder'
9
+ require 'active_support'
10
+
11
+ module Pebblebed
12
+ class HttpError < Exception
13
+ attr_reader :status, :message
14
+ def initialize(message, status = nil)
15
+ @message = message
16
+ @status = status
17
+ end
18
+
19
+ def not_found?
20
+ @status_code == 404
21
+ end
22
+ end
23
+
24
+ module Http
25
+ class CurlResult
26
+ def initialize(curl_result)
27
+ @curl_result = curl_result
28
+ end
29
+
30
+ def status
31
+ @curl_result.response_code
32
+ end
33
+
34
+ def url
35
+ @curl_result.url
36
+ end
37
+
38
+ def body
39
+ @curl_result.body_str
40
+ end
41
+ end
42
+
43
+ def self.get(url = nil, params = nil, &block)
44
+ url, params = url_and_params_from_args(url, params, &block)
45
+ handle_curl_response(Curl::Easy.perform(url_with_params(url, params)))
46
+ end
47
+
48
+ def self.post(url, params, &block)
49
+ url, params = url_and_params_from_args(url, params, &block)
50
+ handle_curl_response(Curl::Easy.http_post(url, *(QueryParams.encode(params).split('&'))))
51
+ end
52
+
53
+ def self.delete(url, params, &block)
54
+ url, params = url_and_params_from_args(url, params, &block)
55
+ handle_curl_response(Curl::Easy.http_delete(url_with_params(url, params)))
56
+ end
57
+
58
+ private
59
+
60
+ def self.handle_http_errors(result)
61
+ if result.status >= 400
62
+ errmsg = "Service request to '#{result.url}' failed (#{result.status}):"
63
+ errmsg << extract_error_summary(result.body)
64
+ raise HttpError.new(result.status, ActiveSupport::SafeBuffer.new(errmsg))
65
+ # ActiveSupport::SafeBuffer.new is the same as errmsg.html_safe in rails
66
+ end
67
+ result
68
+ end
69
+
70
+ def self.handle_curl_response(curl_response)
71
+ handle_http_errors(CurlResult.new(curl_response))
72
+ end
73
+
74
+ def self.url_with_params(url, params)
75
+ url.query = QueryParams.encode(params || {})
76
+ url.to_s
77
+ end
78
+
79
+ def self.url_and_params_from_args(url, params = nil, &block)
80
+ if block_given?
81
+ pathbuilder = PathBuilder.new.send(:instance_eval, &block)
82
+ url = url.dup
83
+ url.path = url.path.chomp("/")+pathbuilder.path
84
+ (params ||= {}).merge!(pathbuilder.params)
85
+ end
86
+ [url, params]
87
+ end
88
+
89
+ def self.extract_error_summary(body)
90
+ # Supports Sinatra error pages
91
+ extract = Nokogiri::HTML(body).css('#summary').text.gsub(/\s+/, ' ').strip
92
+ # TODO: Rails?
93
+ return body if extract == ''
94
+ extract
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,124 @@
1
+ # A class to help consumers of layout parts embed parts
2
+
3
+ class Pebblebed::Parts
4
+
5
+ attr_reader :composition_strategy
6
+
7
+ # A composition strategy is a way to get from an url and a params hash to markup that will
8
+ # render the part into the page. Pluggable strategies to support different production
9
+ # environments. SSI is provided by Nginx, ESI by Varnish and :direct is a fallback
10
+ # for the development environment.
11
+
12
+ @composition_strategies = {}
13
+
14
+ def initialize(connector)
15
+ @composition_strategy = :ssi # <- It's the best! Use Nginx with SSI
16
+ @preloadable = {} # <- A cache to remember which parts are preloadable
17
+ @connector = connector
18
+ end
19
+
20
+ # All part manifests for all configured pebbles as a hash of DeepStructs
21
+ def manifests
22
+ @manifests ||= @connector.quorum.get("/parts")
23
+ @manifests.each { |k,v| @manifests.delete(k) if v.is_a?(Pebblebed::HttpError) }
24
+ @manifests
25
+ end
26
+
27
+ def reload_manifest
28
+ @manifests = nil
29
+ end
30
+
31
+ # The strategy for composing multiple parts into a page. Default: `:ssi`
32
+ def composition_strategy=(value)
33
+ raise ArgumentError, "Unknown composition strategy '#{value}'" unless composition_strategies.keys.include?(value)
34
+ @composition_strategy = value
35
+ end
36
+
37
+ # Generates the markup for the part according to the composition strategy
38
+ def markup(partspec, params = nil)
39
+ ["<div data-pebbles-component=\"#{partspec}\" #{self.class.data_attributes(params || {})}>",
40
+ composition_markup_from_partspec(partspec, params),
41
+ "</div>"].join
42
+ end
43
+
44
+ def stylesheet_urls
45
+ manifests.keys.map do |service|
46
+ @connector[service].service_url("/parts/assets/parts.css")
47
+ end
48
+ end
49
+
50
+ def javascript_urls
51
+ manifests.keys.map do |service|
52
+ @connector[service].service_url("/parts/assets/parts.js")
53
+ end
54
+ end
55
+
56
+ # Register a new composition strategy handler. The block must accept two parameters:
57
+ # |url, params| and is expected to return whatever markup is needed to make it happen.
58
+ def self.register_composition_strategy(name, &block)
59
+ @composition_strategies[name.to_sym] = block
60
+ end
61
+
62
+ private
63
+
64
+ # Check the manifests to see if the part has a server side action implemented.
65
+ def preloadable?(partspec)
66
+ @preloadable[partspec] ||= raw_part_is_preloadable?(partspec)
67
+ end
68
+
69
+ def raw_part_is_preloadable?(partspec)
70
+ service, part = self.class.parse_partspec(partspec)
71
+ return false unless service = manifests[service.to_sym]
72
+ return false unless part_record = service[part]
73
+ return false if part_record.is_a?(::Pebblebed::HttpError)
74
+ return part_record.part.preloadable
75
+ end
76
+
77
+ def composition_markup_from_partspec(partspec, params)
78
+ return '' unless preloadable?(partspec)
79
+ service, part = self.class.parse_partspec(partspec)
80
+ composition_markup(
81
+ @connector[service].service_url("/parts/#{part}"), params)
82
+ end
83
+
84
+ def self.composition_strategies
85
+ @composition_strategies
86
+ end
87
+
88
+ def composition_markup(url, params)
89
+ self.class.composition_strategies[@composition_strategy].call(url, params)
90
+ end
91
+
92
+ def self.parse_partspec(partspec)
93
+ /^(?<service>[^\.]+)\.(?<part>.*)$/ =~ partspec
94
+ [service, part]
95
+ end
96
+
97
+ # Create a string of data-attributes from a hash
98
+ def self.data_attributes(hash)
99
+ hash = hash.dup
100
+ hash.select{ |k| k != :session }.
101
+ map { |k,v| "data-#{k.to_s}=\"#{v}\"" }.join(' ')
102
+ end
103
+ end
104
+
105
+ # -------------------------------------------------------------------------------
106
+
107
+ # SSI (Nginx): http://wiki.nginx.org/HttpSsiModule
108
+ Pebblebed::Parts.register_composition_strategy :ssi do |url, params|
109
+ "<!--# include virtual=\"#{URI.parse(url.to_s).path}?#{QueryParams.encode(params || {})}\" -->"
110
+ end
111
+
112
+ # ESI (Varnish): https://www.varnish-cache.org/trac/wiki/ESIfeatures
113
+ Pebblebed::Parts.register_composition_strategy :esi do |url, params|
114
+ "<esi:include src=\"#{URI.parse(url.to_s).path}?#{QueryParams.encode(params || {})}\"\/>"
115
+ end
116
+
117
+ # Just fetches the content and returns it. ONLY FOR DEVELOPMENT
118
+ Pebblebed::Parts.register_composition_strategy :direct do |url, params|
119
+ begin
120
+ Pebblebed::Connector.new.get(url, params)
121
+ rescue HttpError => e
122
+ "<span class='pebbles_error'>'#{url}' with parameters #{params.to_json} failed (#{e.status}): #{e.message}</span>"
123
+ end
124
+ end
@@ -0,0 +1,79 @@
1
+ # Extends Sinatra for maximum pebble pleasure
2
+ require 'pebblebed'
3
+
4
+ module Sinatra
5
+ module Pebblebed
6
+ module Helpers
7
+ # Render the markup for a part. A partspec takes the form
8
+ # "<kit>.<partname>", e.g. "base.post"
9
+ def part(partspec, params = {})
10
+ params[:session] ||= current_session
11
+ pebbles.parts.markup(partspec, params)
12
+ end
13
+
14
+ def parts_script_include_tags
15
+ @script_include_tags ||= pebbles.parts.javascript_urls.map do |url|
16
+ "<script src=\"#{url.to_s}\"></script>"
17
+ end.join
18
+ end
19
+
20
+ def parts_stylesheet_include_tags
21
+ @stylesheet_include_tags ||= pebbles.parts.stylesheet_urls.map do |url|
22
+ "<link rel=\"stylesheet\" type=\"text/css\" media=\"all\" href=\"#{url.to_s}\">"
23
+ end.join
24
+ end
25
+
26
+ def current_session
27
+ params[:session] || request.cookies['checkpoint.session']
28
+ end
29
+ alias :checkpoint_session :current_session
30
+
31
+ def pebbles
32
+ @pebbles ||= ::Pebblebed::Connector.new(checkpoint_session, :host => request.host)
33
+ end
34
+
35
+ def current_identity
36
+ pebbles.checkpoint.me
37
+ end
38
+
39
+ def require_identity
40
+ unless current_identity.respond_to?(:id)
41
+ halt 403, "No current identity."
42
+ end
43
+ end
44
+
45
+ def current_identity_is?(identity_id)
46
+ require_identity
47
+ unless (current_identity.id == identity_id || current_identity.god)
48
+ halt 403, "Private resource"
49
+ end
50
+ end
51
+
52
+ def require_god
53
+ require_identity
54
+ halt 403, "Current identity #{current_identity.id} is not god" unless current_identity.god
55
+ end
56
+
57
+ def require_parameters(parameters, *keys)
58
+ missing = keys.map(&:to_s) - (parameters ? parameters.keys : [])
59
+ halt 409, "missing parameters: #{missing.join(', ')}" unless missing.empty?
60
+ end
61
+ end
62
+
63
+ def self.registered(app)
64
+ app.helpers(Sinatra::Pebblebed::Helpers)
65
+ app.get "/ping" do
66
+ "{\"name\":#{(app.service_name || 'undefined').to_s.inspect}}"
67
+ end
68
+ end
69
+
70
+ def declare_pebbles(&block)
71
+ ::Pebblebed.config(&block)
72
+ end
73
+
74
+ def i_am(service_name)
75
+ set :service_name, service_name
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,76 @@
1
+ module Pebblebed
2
+ class InvalidUid < StandardError; end
3
+ class Uid
4
+ def initialize(uid)
5
+ self.klass, self.path, self.oid = self.class.raw_parse(uid)
6
+ raise InvalidUid, "Missing klass in uid" unless self.klass
7
+ raise InvalidUid, "A valid uid must specify either path or oid" unless self.path || self.oid
8
+ end
9
+
10
+ attr_reader :klass, :path, :oid
11
+ def klass=(value)
12
+ return @klass = nil if value == '' || value.nil?
13
+ raise InvalidUid, "Invalid klass '#{value}'" unless self.class.valid_klass?(value)
14
+ @klass = value
15
+ end
16
+ def path=(value)
17
+ return @path = nil if value == '' || value.nil?
18
+ raise InvalidUid, "Invalid path '#{value}'" unless self.class.valid_path?(value)
19
+ @path = (value.strip != "") ? value : nil
20
+ end
21
+ def oid=(value)
22
+ return @oid = nil if value == '' || value.nil?
23
+ raise InvalidUid, "Invalid oid '#{value}'" unless self.class.valid_oid?(value)
24
+ @oid = (value.strip != "") ? value : nil
25
+ end
26
+
27
+ def self.raw_parse(string)
28
+ /(?<klass>^[^:]+)\:(?<path>[^\$]*)?\$?(?<oid>.*$)?/ =~ string
29
+ [klass, path, oid]
30
+ end
31
+
32
+ def self.valid?(string)
33
+ begin
34
+ true if new(string)
35
+ rescue InvalidUid
36
+ false
37
+ end
38
+ end
39
+
40
+ def self.parse(string)
41
+ uid = new(string)
42
+ [uid.klass, uid.path, uid.oid]
43
+ end
44
+
45
+ def self.valid_label?(value)
46
+ !!(value =~ /^[a-zA-Z0-9_]+$/)
47
+ end
48
+
49
+ def self.valid_klass?(value)
50
+ self.valid_label?(value)
51
+ end
52
+
53
+ def self.valid_path?(value)
54
+ # catches a stupid edge case in ruby where "..".split('.') == [] instead of ["", "", ""]
55
+ return false if value =~ /^\.+$/
56
+ value.split('.').each do |label|
57
+ return false unless self.valid_label?(label)
58
+ end
59
+ true
60
+ end
61
+
62
+ def self.valid_oid?(value)
63
+ self.valid_label?(value)
64
+ end
65
+
66
+ def inspect
67
+ "#<Pebblebed::Uid '#{to_s}'>"
68
+ end
69
+
70
+ def to_s
71
+ "#{@klass}:#{@path}$#{@oid}".chomp("$")
72
+ end
73
+ alias_method :to_uid, :to_s
74
+
75
+ end
76
+ end
@@ -0,0 +1,3 @@
1
+ module Pebblebed
2
+ VERSION = "0.0.1"
3
+ end
data/lib/pebblebed.rb ADDED
@@ -0,0 +1,10 @@
1
+ require "pebblebed/version"
2
+ require 'pebblebed/http'
3
+ require 'pebblebed/connector'
4
+ require 'pebblebed/config'
5
+ require 'pebblebed/uid'
6
+ require 'pebblebed/clients/abstract_client'
7
+ require 'pebblebed/clients/generic_client'
8
+ require 'pebblebed/clients/checkpoint_client'
9
+ require 'pebblebed/clients/quorum_client'
10
+ require 'pebblebed/parts'
data/pebblebed.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "pebblebed/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "pebblebed"
7
+ s.version = Pebblebed::VERSION
8
+ s.authors = ["Katrina Owen", "Simen Svale Skogsrud"]
9
+ s.email = ["katrina@bengler.no", "simen@bengler.no"]
10
+ s.homepage = ""
11
+ s.summary = %q{Development tools for working with Pebblebed}
12
+ s.description = %q{Development tools for working with Pebblebed}
13
+
14
+ s.rubyforge_project = "pebblebed"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "simplecov"
25
+
26
+ s.add_runtime_dependency "deepstruct"
27
+ s.add_runtime_dependency "curb"
28
+ s.add_runtime_dependency "yajl-ruby"
29
+ s.add_runtime_dependency "queryparams"
30
+ s.add_runtime_dependency "futurevalue"
31
+ s.add_runtime_dependency "pathbuilder"
32
+ s.add_runtime_dependency "nokogiri"
33
+ s.add_runtime_dependency "i18n"
34
+ s.add_runtime_dependency "activesupport"
35
+
36
+ end
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+
4
+ describe Pebblebed::CheckpointClient do
5
+
6
+ let(:checkpoint_client) { Pebblebed::Connector.new('session_key')[:checkpoint] }
7
+
8
+ describe "me" do
9
+ let(:canned_response_for_me) {
10
+ DeepStruct.wrap({:body=>{:identity => {:id => 1, :god => true}}.to_json})
11
+ }
12
+
13
+ it "returns current user identity upon request and caches it as an instance variable" do
14
+ checkpoint_client = Pebblebed::Connector.new('session_key')[:checkpoint]
15
+
16
+ Pebblebed::Http.should_receive(:get) { |url|
17
+ url.path.should match("/identities/me")
18
+ canned_response_for_me
19
+ }.once
20
+ checkpoint_client.me
21
+ checkpoint_client.me
22
+ end
23
+
24
+ it "tells us whether we are dealing with god allmighty himself or just another average joe" do
25
+ checkpoint_client = Pebblebed::Connector.new('session_key')[:checkpoint]
26
+ Pebblebed::Http.should_receive(:get) { |url|
27
+ url.path.should match("/identities/me")
28
+ canned_response_for_me
29
+ }.once
30
+ checkpoint_client.god?.should eq true
31
+ end
32
+ end
33
+
34
+ describe "cache_key_for_identity_id" do
35
+ it "creates a nice looking cache key for memcache" do
36
+ checkpoint_client = Pebblebed::Connector.new('session_key')[:checkpoint]
37
+ checkpoint_client.cache_key_for_identity_id(2).should eq "identity:2"
38
+ end
39
+ end
40
+
41
+ describe "find_identities" do
42
+ let(:canned_response) {
43
+ DeepStruct.wrap({:body=>
44
+ {:identities =>
45
+ [{:identity => {:id => 1}}, {:identity => {}}, {:identity => {:id => 3}}, {:identity => {}}]
46
+ }.to_json
47
+ })
48
+ }
49
+
50
+ describe "without memcache configured" do
51
+ before(:each) do
52
+ Pebblebed.config do
53
+ host "checkpoint.dev"
54
+ service :checkpoint
55
+ end
56
+ end
57
+
58
+ it "issues an http request every time" do
59
+ Pebblebed::Http.should_receive(:get).twice.and_return canned_response
60
+ checkpoint_client.find_identities([1, 2])
61
+ checkpoint_client.find_identities([1, 2])
62
+ end
63
+ end
64
+
65
+ describe "with memcached configured" do
66
+ before(:each) do
67
+ Pebblebed.config do
68
+ host "checkpoint.dev"
69
+ memcached $memcached
70
+ service :checkpoint
71
+ end
72
+ end
73
+
74
+ it "issues an http request and caches it" do
75
+ Pebblebed::Http.should_receive(:get).once.and_return(canned_response)
76
+ checkpoint_client.find_identities([1, 2])
77
+ checkpoint_client.find_identities([1, 2])
78
+
79
+ $memcached.get(checkpoint_client.cache_key_for_identity_id(1)).should eq({"id"=>1})
80
+ $memcached.get(checkpoint_client.cache_key_for_identity_id(2)).should eq({})
81
+ end
82
+
83
+ it "returns exactly the same data no matter if it is cached or originating from a request" do
84
+ Pebblebed::Http.should_receive(:get).once.and_return(canned_response)
85
+
86
+ http_requested_result = checkpoint_client.find_identities([1, 2, 3])
87
+ cached_result = checkpoint_client.find_identities([1, 2, 3])
88
+
89
+ http_requested_result.unwrap.should eq cached_result.unwrap
90
+ end
91
+
92
+ it "issues a request only for not previously cached identities" do
93
+ Pebblebed::Http.should_receive(:get) { |url|
94
+ url.path.should match("/identities/1,2,4")
95
+ canned_response
96
+ }.once
97
+ checkpoint_client.find_identities([1, 2, 4])
98
+
99
+ Pebblebed::Http.should_receive(:get) { |url|
100
+ url.path.should match("/identities/3,") # Note the extra comma. Will ensure that a list is returned
101
+ canned_response
102
+ }.once
103
+ checkpoint_client.find_identities([1, 2, 3, 4])
104
+ end
105
+
106
+ it "will always return identities in the order they are requested" do
107
+ Pebblebed::Http.should_receive(:get).once.and_return(canned_response)
108
+
109
+ checkpoint_client.find_identities([1, 2, 3, 4])
110
+ identities = checkpoint_client.find_identities([4, 3, 2, 1])
111
+ identities[1].id.should eq 3
112
+ identities[3].id.should eq 1
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pebblebed do
4
+ it "has a nice dsl that configures stuff" do
5
+ Pebblebed.config do
6
+ host "example.org"
7
+ memcached $memcached
8
+ service :checkpoint
9
+ end
10
+
11
+ Pebblebed.host.should eq "example.org"
12
+ Pebblebed.memcached.should eq $memcached
13
+ Pebblebed::Connector.instance_methods.should include :checkpoint
14
+ end
15
+
16
+ it "can calculate the root uri of any pebble" do
17
+ Pebblebed.config do
18
+ service :checkpoint
19
+ service :foobar, :version => 2
20
+ end
21
+ Pebblebed.host = "example.org"
22
+ Pebblebed.root_url_for(:checkpoint).to_s.should eq "http://example.org/api/checkpoint/v1/"
23
+ Pebblebed.root_url_for(:checkpoint, :host => 'checkpoint.dev').to_s.should eq "http://checkpoint.dev/api/checkpoint/v1/"
24
+ Pebblebed.root_url_for(:foobar).to_s.should eq "http://example.org/api/foobar/v2/"
25
+ end
26
+
27
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Pebblebed::Connector" do
4
+ it "can configure clients for any service" do
5
+ connector = Pebblebed::Connector.new("session_key")
6
+ client = connector['foobar']
7
+ client.class.name.should eq "Pebblebed::GenericClient"
8
+ client.instance_variable_get(:@session_key).should eq "session_key"
9
+ client.instance_variable_get(:@root_url).to_s.should =~ /api\/foobar/
10
+ end
11
+
12
+ it "caches any given client" do
13
+ connector = Pebblebed::Connector.new("session_key")
14
+ connector['foobar'].should eq connector['foobar']
15
+ end
16
+
17
+ it "fetches specific client implementations if one is provided" do
18
+ connector = Pebblebed::Connector.new("session_key")
19
+ connector['checkpoint'].class.name.should eq "Pebblebed::CheckpointClient"
20
+ connector['foobar'].class.name.should eq "Pebblebed::GenericClient"
21
+ end
22
+
23
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pebblebed::GenericClient do
4
+ it "always forwards the session key" do
5
+ client = Pebblebed::GenericClient.new("session_key", "http://example.org/")
6
+ client.service_params({})['session'].should eq "session_key"
7
+ end
8
+
9
+ it "always converts urls to URI-objects" do
10
+ client = Pebblebed::GenericClient.new("session_key", "http://example.org/")
11
+ client.instance_variable_get(:@root_url).class.name.should eq ("URI::HTTP")
12
+ end
13
+
14
+ it "knows how to generate a service specific url" do
15
+ client = Pebblebed::GenericClient.new("session_key", "http://example.org/")
16
+ client.service_url("/test").to_s.should eq "http://example.org/test"
17
+ client.service_url("").to_s.should eq "http://example.org"
18
+ end
19
+
20
+ it "wraps JSON-results in a deep struct" do
21
+ curl_result = DeepStruct.wrap({status:200, body:'{"hello":"world"}'})
22
+ Pebblebed::Http.stub(:get).and_return(curl_result)
23
+ client = Pebblebed::GenericClient.new("session_key", "http://example.org/")
24
+ result = client.get "/"
25
+ result.class.name.should eq "DeepStruct::HashWrapper"
26
+ result.hello.should eq "world"
27
+ end
28
+
29
+ it "does not wrap non-json results" do
30
+ curl_result = DeepStruct.wrap({status:200, body:'Ok'})
31
+ Pebblebed::Http.stub(:get).and_return(curl_result)
32
+ client = Pebblebed::GenericClient.new("session_key", "http://example.org/")
33
+ result = client.get "/"
34
+ result.class.name.should eq "String"
35
+ result.should eq "Ok"
36
+ end
37
+
38
+ end
data/spec/http_spec.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pebblebed::Http do
4
+ it "knows how to pack params into a http query string" do
5
+ Pebblebed::Http.send(:url_with_params, URI("/dingo/"), {a:1}).should eq "/dingo/?a=1"
6
+ end
7
+
8
+ it "knows how to combine url and parmas with results of pathbuilder" do
9
+ url, params = Pebblebed::Http.send(:url_and_params_from_args, URI("http://example.org/api"), {a:1}) do
10
+ foo.bar(:b => 2)
11
+ end
12
+ params.should eq(:a => 1, :b => 2)
13
+ url.to_s.should eq "http://example.org/api/foo/bar"
14
+ end
15
+
16
+ it "raises an exception if there is a http-error" do
17
+ -> { Pebblebed::Http.send(:handle_http_errors, DeepStruct.wrap(status:400, url:"/foobar", body:"Oh noes")) }.should raise_error Pebblebed::HttpError
18
+ end
19
+
20
+ end
@@ -0,0 +1,25 @@
1
+ class Mockcached
2
+ def initialize
3
+ @store = {}
4
+ end
5
+
6
+ def set(*args)
7
+ @store[args[0]] = args[1]
8
+ end
9
+
10
+ def get(*args)
11
+ @store[args[0]]
12
+ end
13
+
14
+ def get_multi(*keys)
15
+ result = {}
16
+ keys.each do |key|
17
+ result[key] = get(key) if @store.has_key?(key)
18
+ end
19
+ result
20
+ end
21
+
22
+ def delete(*args)
23
+ @store.delete(args[0])
24
+ end
25
+ end
@@ -0,0 +1,19 @@
1
+ require 'simplecov'
2
+ require './spec/mockcached'
3
+ require 'bundler'
4
+ require 'rspec'
5
+
6
+ SimpleCov.add_filter 'spec'
7
+ SimpleCov.add_filter 'config'
8
+ SimpleCov.start
9
+
10
+ Bundler.require
11
+
12
+ RSpec.configure do |c|
13
+ c.mock_with :rspec
14
+ c.around(:each) do |example|
15
+ clear_cookies if respond_to?(:clear_cookies)
16
+ $memcached = Mockcached.new
17
+ example.run
18
+ end
19
+ end
data/spec/uid_spec.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+ describe Pebblebed::Uid do
4
+ it "parses a full uid correctly" do
5
+ uid = Pebblebed::Uid.new("klass:path$oid")
6
+ uid.klass.should eq "klass"
7
+ uid.path.should eq "path"
8
+ uid.oid.should eq "oid"
9
+ uid.to_s.should eq "klass:path$oid"
10
+ end
11
+
12
+ it "parses an uid with no oid correctly" do
13
+ uid = Pebblebed::Uid.new("klass:path")
14
+ uid.klass.should eq "klass"
15
+ uid.path.should eq "path"
16
+ uid.oid.should be_nil
17
+ uid.to_s.should eq "klass:path"
18
+ end
19
+
20
+ it "parses an uid with no path correctly" do
21
+ uid = Pebblebed::Uid.new("klass:$oid")
22
+ uid.klass.should eq "klass"
23
+ uid.path.should be_nil
24
+ uid.oid.should eq "oid"
25
+ uid.to_s.should eq "klass:$oid"
26
+ end
27
+
28
+ it "raises an exception when you try to create an invalid uid" do
29
+ -> { Pebblebed::Uid.new("!:$298") }.should raise_error Pebblebed::InvalidUid
30
+ end
31
+
32
+ it "raises an exception when you modify an uid with an invalid value" do
33
+ uid = Pebblebed::Uid.new("klass:path$oid")
34
+ -> { uid.klass = "!" }.should raise_error Pebblebed::InvalidUid
35
+ -> { uid.path = "..." }.should raise_error Pebblebed::InvalidUid
36
+ -> { uid.oid = "(/&%$" }.should raise_error Pebblebed::InvalidUid
37
+ end
38
+
39
+ it "rejects invalid labels for klass and oid" do
40
+ Pebblebed::Uid.valid_klass?("abc123").should be_true
41
+ Pebblebed::Uid.valid_klass?("abc123!").should be_false
42
+ Pebblebed::Uid.valid_klass?("").should be_false
43
+ Pebblebed::Uid.valid_oid?("abc123").should be_true
44
+ Pebblebed::Uid.valid_oid?("abc123!").should be_false
45
+ Pebblebed::Uid.valid_oid?("abc 123").should be_false
46
+ Pebblebed::Uid.valid_oid?("").should be_false
47
+ end
48
+
49
+ it "rejects invalid paths" do
50
+ Pebblebed::Uid.valid_path?("abc123").should be_true
51
+ Pebblebed::Uid.valid_path?("abc.123").should be_true
52
+ Pebblebed::Uid.valid_path?("").should be_true
53
+ Pebblebed::Uid.valid_path?("abc!.").should be_false
54
+ Pebblebed::Uid.valid_path?(".").should be_false
55
+ Pebblebed::Uid.valid_path?("ab. 123").should be_false
56
+ end
57
+
58
+ it "knows how to parse in place" do
59
+ Pebblebed::Uid.parse("klass:path$oid").should eq ['klass', 'path', 'oid']
60
+ Pebblebed::Uid.parse("post:this.is.a.path.to$object_id").should eq ['post', 'this.is.a.path.to', 'object_id']
61
+ Pebblebed::Uid.parse("post:$object_id").should eq ['post', nil, 'object_id']
62
+ end
63
+
64
+ it "knows the valid uids from the invalid ones" do
65
+ Pebblebed::Uid.valid?("F**ing H%$#!!!").should be_false
66
+ Pebblebed::Uid.valid?("").should be_false
67
+ Pebblebed::Uid.valid?("bang:").should be_false
68
+ Pebblebed::Uid.valid?(":bang").should be_false
69
+ Pebblebed::Uid.valid?(":bang$paff").should be_false
70
+ Pebblebed::Uid.valid?("$paff").should be_false
71
+ Pebblebed::Uid.valid?("a:b.c.d$e").should be_true
72
+ Pebblebed::Uid.valid?("a:$e").should be_true
73
+ Pebblebed::Uid.valid?("a:b.c.d").should be_true
74
+ end
75
+
76
+ end
metadata ADDED
@@ -0,0 +1,212 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pebblebed
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Katrina Owen
9
+ - Simen Svale Skogsrud
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-12-18 00:00:00.000000000Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ requirement: &70362516223620 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *70362516223620
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake
28
+ requirement: &70362516220480 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *70362516220480
37
+ - !ruby/object:Gem::Dependency
38
+ name: simplecov
39
+ requirement: &70362516217900 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *70362516217900
48
+ - !ruby/object:Gem::Dependency
49
+ name: deepstruct
50
+ requirement: &70362516215200 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: *70362516215200
59
+ - !ruby/object:Gem::Dependency
60
+ name: curb
61
+ requirement: &70362516213740 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: *70362516213740
70
+ - !ruby/object:Gem::Dependency
71
+ name: yajl-ruby
72
+ requirement: &70362516211060 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: *70362516211060
81
+ - !ruby/object:Gem::Dependency
82
+ name: queryparams
83
+ requirement: &70362516207240 !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: *70362516207240
92
+ - !ruby/object:Gem::Dependency
93
+ name: futurevalue
94
+ requirement: &70362516206260 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ! '>='
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ type: :runtime
101
+ prerelease: false
102
+ version_requirements: *70362516206260
103
+ - !ruby/object:Gem::Dependency
104
+ name: pathbuilder
105
+ requirement: &70362516175980 !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ type: :runtime
112
+ prerelease: false
113
+ version_requirements: *70362516175980
114
+ - !ruby/object:Gem::Dependency
115
+ name: nokogiri
116
+ requirement: &70362516172200 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ! '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ type: :runtime
123
+ prerelease: false
124
+ version_requirements: *70362516172200
125
+ - !ruby/object:Gem::Dependency
126
+ name: i18n
127
+ requirement: &70362516170520 !ruby/object:Gem::Requirement
128
+ none: false
129
+ requirements:
130
+ - - ! '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :runtime
134
+ prerelease: false
135
+ version_requirements: *70362516170520
136
+ - !ruby/object:Gem::Dependency
137
+ name: activesupport
138
+ requirement: &70362516163800 !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ! '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ type: :runtime
145
+ prerelease: false
146
+ version_requirements: *70362516163800
147
+ description: Development tools for working with Pebblebed
148
+ email:
149
+ - katrina@bengler.no
150
+ - simen@bengler.no
151
+ executables: []
152
+ extensions: []
153
+ extra_rdoc_files: []
154
+ files:
155
+ - .gitignore
156
+ - .rspec
157
+ - Gemfile
158
+ - Rakefile
159
+ - lib/pebblebed.rb
160
+ - lib/pebblebed/clients/abstract_client.rb
161
+ - lib/pebblebed/clients/checkpoint_client.rb
162
+ - lib/pebblebed/clients/generic_client.rb
163
+ - lib/pebblebed/clients/quorum_client.rb
164
+ - lib/pebblebed/config.rb
165
+ - lib/pebblebed/connector.rb
166
+ - lib/pebblebed/http.rb
167
+ - lib/pebblebed/parts.rb
168
+ - lib/pebblebed/sinatra.rb
169
+ - lib/pebblebed/uid.rb
170
+ - lib/pebblebed/version.rb
171
+ - pebblebed.gemspec
172
+ - spec/checkpoint_client_spec.rb
173
+ - spec/config_spec.rb
174
+ - spec/connector_spec.rb
175
+ - spec/generic_client_spec.rb
176
+ - spec/http_spec.rb
177
+ - spec/mockcached.rb
178
+ - spec/spec_helper.rb
179
+ - spec/uid_spec.rb
180
+ homepage: ''
181
+ licenses: []
182
+ post_install_message:
183
+ rdoc_options: []
184
+ require_paths:
185
+ - lib
186
+ required_ruby_version: !ruby/object:Gem::Requirement
187
+ none: false
188
+ requirements:
189
+ - - ! '>='
190
+ - !ruby/object:Gem::Version
191
+ version: '0'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ requirements: []
199
+ rubyforge_project: pebblebed
200
+ rubygems_version: 1.8.10
201
+ signing_key:
202
+ specification_version: 3
203
+ summary: Development tools for working with Pebblebed
204
+ test_files:
205
+ - spec/checkpoint_client_spec.rb
206
+ - spec/config_spec.rb
207
+ - spec/connector_spec.rb
208
+ - spec/generic_client_spec.rb
209
+ - spec/http_spec.rb
210
+ - spec/mockcached.rb
211
+ - spec/spec_helper.rb
212
+ - spec/uid_spec.rb