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 +8 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/lib/pebblebed/clients/abstract_client.rb +21 -0
- data/lib/pebblebed/clients/checkpoint_client.rb +45 -0
- data/lib/pebblebed/clients/generic_client.rb +33 -0
- data/lib/pebblebed/clients/quorum_client.rb +30 -0
- data/lib/pebblebed/config.rb +57 -0
- data/lib/pebblebed/connector.rb +38 -0
- data/lib/pebblebed/http.rb +98 -0
- data/lib/pebblebed/parts.rb +124 -0
- data/lib/pebblebed/sinatra.rb +79 -0
- data/lib/pebblebed/uid.rb +76 -0
- data/lib/pebblebed/version.rb +3 -0
- data/lib/pebblebed.rb +10 -0
- data/pebblebed.gemspec +36 -0
- data/spec/checkpoint_client_spec.rb +116 -0
- data/spec/config_spec.rb +27 -0
- data/spec/connector_spec.rb +23 -0
- data/spec/generic_client_spec.rb +38 -0
- data/spec/http_spec.rb +20 -0
- data/spec/mockcached.rb +25 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/uid_spec.rb +76 -0
- metadata +212 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
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
|
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
|
data/spec/config_spec.rb
ADDED
@@ -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
|
data/spec/mockcached.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|