pebblebed 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/.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