friendly 0.3.3
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/.document +5 -0
- data/.gitignore +23 -0
- data/APACHE-LICENSE +202 -0
- data/LICENSE +20 -0
- data/README.md +173 -0
- data/Rakefile +67 -0
- data/VERSION +1 -0
- data/examples/friendly.yml +7 -0
- data/friendly.gemspec +201 -0
- data/lib/friendly/attribute.rb +65 -0
- data/lib/friendly/boolean.rb +6 -0
- data/lib/friendly/cache/by_id.rb +33 -0
- data/lib/friendly/cache.rb +24 -0
- data/lib/friendly/config.rb +5 -0
- data/lib/friendly/data_store.rb +72 -0
- data/lib/friendly/document.rb +165 -0
- data/lib/friendly/document_table.rb +56 -0
- data/lib/friendly/index.rb +73 -0
- data/lib/friendly/memcached.rb +48 -0
- data/lib/friendly/newrelic.rb +6 -0
- data/lib/friendly/query.rb +42 -0
- data/lib/friendly/sequel_monkey_patches.rb +35 -0
- data/lib/friendly/storage.rb +31 -0
- data/lib/friendly/storage_factory.rb +24 -0
- data/lib/friendly/storage_proxy.rb +103 -0
- data/lib/friendly/table.rb +15 -0
- data/lib/friendly/table_creator.rb +43 -0
- data/lib/friendly/time.rb +14 -0
- data/lib/friendly/translator.rb +32 -0
- data/lib/friendly/uuid.rb +143 -0
- data/lib/friendly.rb +49 -0
- data/rails/init.rb +3 -0
- data/spec/fakes/data_store_fake.rb +29 -0
- data/spec/fakes/database_fake.rb +12 -0
- data/spec/fakes/dataset_fake.rb +28 -0
- data/spec/fakes/document.rb +18 -0
- data/spec/fakes/serializer_fake.rb +12 -0
- data/spec/fakes/time_fake.rb +12 -0
- data/spec/integration/basic_object_lifecycle_spec.rb +114 -0
- data/spec/integration/batch_insertion_spec.rb +29 -0
- data/spec/integration/convenience_api_spec.rb +25 -0
- data/spec/integration/count_spec.rb +12 -0
- data/spec/integration/default_value_spec.rb +15 -0
- data/spec/integration/find_via_cache_spec.rb +101 -0
- data/spec/integration/finder_spec.rb +64 -0
- data/spec/integration/index_spec.rb +57 -0
- data/spec/integration/pagination_spec.rb +63 -0
- data/spec/integration/table_creator_spec.rb +52 -0
- data/spec/integration/write_through_cache_spec.rb +53 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +90 -0
- data/spec/unit/attribute_spec.rb +64 -0
- data/spec/unit/cache_by_id_spec.rb +102 -0
- data/spec/unit/cache_spec.rb +21 -0
- data/spec/unit/config_spec.rb +4 -0
- data/spec/unit/data_store_spec.rb +188 -0
- data/spec/unit/document_spec.rb +311 -0
- data/spec/unit/document_table_spec.rb +126 -0
- data/spec/unit/friendly_spec.rb +25 -0
- data/spec/unit/index_spec.rb +196 -0
- data/spec/unit/memcached_spec.rb +114 -0
- data/spec/unit/query_spec.rb +104 -0
- data/spec/unit/storage_factory_spec.rb +59 -0
- data/spec/unit/storage_proxy_spec.rb +218 -0
- data/spec/unit/translator_spec.rb +96 -0
- data/website/index.html +210 -0
- data/website/scripts/clipboard.swf +0 -0
- data/website/scripts/shBrushAS3.js +61 -0
- data/website/scripts/shBrushBash.js +66 -0
- data/website/scripts/shBrushCSharp.js +67 -0
- data/website/scripts/shBrushColdFusion.js +102 -0
- data/website/scripts/shBrushCpp.js +99 -0
- data/website/scripts/shBrushCss.js +93 -0
- data/website/scripts/shBrushDelphi.js +57 -0
- data/website/scripts/shBrushDiff.js +43 -0
- data/website/scripts/shBrushErlang.js +54 -0
- data/website/scripts/shBrushGroovy.js +69 -0
- data/website/scripts/shBrushJScript.js +52 -0
- data/website/scripts/shBrushJava.js +59 -0
- data/website/scripts/shBrushJavaFX.js +60 -0
- data/website/scripts/shBrushPerl.js +74 -0
- data/website/scripts/shBrushPhp.js +91 -0
- data/website/scripts/shBrushPlain.js +35 -0
- data/website/scripts/shBrushPowerShell.js +76 -0
- data/website/scripts/shBrushPython.js +66 -0
- data/website/scripts/shBrushRuby.js +57 -0
- data/website/scripts/shBrushScala.js +53 -0
- data/website/scripts/shBrushSql.js +68 -0
- data/website/scripts/shBrushVb.js +58 -0
- data/website/scripts/shBrushXml.js +71 -0
- data/website/scripts/shCore.js +30 -0
- data/website/scripts/shLegacy.js +30 -0
- data/website/styles/friendly.css +103 -0
- data/website/styles/help.png +0 -0
- data/website/styles/ie.css +35 -0
- data/website/styles/magnifier.png +0 -0
- data/website/styles/page_white_code.png +0 -0
- data/website/styles/page_white_copy.png +0 -0
- data/website/styles/print.css +29 -0
- data/website/styles/printer.png +0 -0
- data/website/styles/screen.css +257 -0
- data/website/styles/shCore.css +330 -0
- data/website/styles/shThemeDefault.css +173 -0
- data/website/styles/shThemeDjango.css +176 -0
- data/website/styles/shThemeEclipse.css +190 -0
- data/website/styles/shThemeEmacs.css +175 -0
- data/website/styles/shThemeFadeToGrey.css +177 -0
- data/website/styles/shThemeMidnight.css +175 -0
- data/website/styles/shThemeRDark.css +175 -0
- metadata +264 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require 'friendly/table'
|
|
2
|
+
|
|
3
|
+
module Friendly
|
|
4
|
+
class Index < Table
|
|
5
|
+
attr_reader :klass, :fields, :datastore
|
|
6
|
+
|
|
7
|
+
def initialize(klass, fields, datastore = Friendly.datastore)
|
|
8
|
+
@klass = klass
|
|
9
|
+
@fields = fields
|
|
10
|
+
@datastore = datastore
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def table_name
|
|
14
|
+
["index", klass.table_name, "on", fields.join("_and_")].join("_")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def satisfies?(query)
|
|
18
|
+
exact_match?(query) || valid_partial_match?(query)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def first(query)
|
|
22
|
+
row = datastore.first(self, query)
|
|
23
|
+
row && klass.first(:id => row[:id])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def all(query)
|
|
27
|
+
ids = datastore.all(self, query).map { |row| row[:id] }
|
|
28
|
+
klass.all(:id => ids, :preserve_order! => !query.order.nil?)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def count(query)
|
|
32
|
+
datastore.count(self, query)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create(document)
|
|
36
|
+
datastore.insert(self, record(document))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def update(document)
|
|
40
|
+
datastore.update(self, document.id, record(document))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy(document)
|
|
44
|
+
datastore.delete(self, document.id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
protected
|
|
48
|
+
def exact_match?(query)
|
|
49
|
+
query.conditions.keys.map { |f| f.to_s }.sort ==
|
|
50
|
+
fields.map { |f| f.to_s }.sort &&
|
|
51
|
+
valid_order?(query.order)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def valid_partial_match?(query)
|
|
55
|
+
condition_fields = query.conditions.keys
|
|
56
|
+
sorted = condition_fields.sort { |a,b| field_index(a) <=> field_index(b) }
|
|
57
|
+
sorted << query.order.expression if query.order
|
|
58
|
+
sorted.zip(fields).all? { |a,b| a == b }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def valid_order?(order)
|
|
62
|
+
order.nil? || order.expression == fields.last
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def field_index(attr)
|
|
66
|
+
fields.index(attr) || 0
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def record(document)
|
|
70
|
+
Hash[*(fields + [:id]).map { |f| [f, document.send(f)] }.flatten]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class Memcached
|
|
3
|
+
attr_reader :cache
|
|
4
|
+
|
|
5
|
+
def initialize(cache)
|
|
6
|
+
@cache = cache
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def set(key, value)
|
|
10
|
+
@cache.set(key, value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(key)
|
|
14
|
+
@cache.get(key)
|
|
15
|
+
rescue ::Memcached::NotFound
|
|
16
|
+
if block_given?
|
|
17
|
+
miss(key) { yield }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def multiget(keys)
|
|
22
|
+
return {} if keys.empty?
|
|
23
|
+
|
|
24
|
+
hits = @cache.get(keys)
|
|
25
|
+
missing_keys = keys - hits.keys
|
|
26
|
+
|
|
27
|
+
if !missing_keys.empty? && block_given?
|
|
28
|
+
missing_keys.each do |missing_key|
|
|
29
|
+
hits.merge!(missing_key => miss(missing_key) { yield(missing_key) })
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
hits
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def delete(key)
|
|
37
|
+
cache.delete(key)
|
|
38
|
+
rescue ::Memcached::NotFound
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
protected
|
|
42
|
+
def miss(key)
|
|
43
|
+
value = yield
|
|
44
|
+
@cache.set(key, value)
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class Query
|
|
3
|
+
attr_reader :conditions, :limit, :order,
|
|
4
|
+
:preserve_order, :offset, :uuid_klass,
|
|
5
|
+
:page, :per_page
|
|
6
|
+
|
|
7
|
+
def initialize(parameters, uuid_klass = UUID)
|
|
8
|
+
@uuid_klass = uuid_klass
|
|
9
|
+
@conditions = parameters.reject { |k,v| k.to_s =~ /!$/ }
|
|
10
|
+
@page = (parameters[:page!] || 1).to_i
|
|
11
|
+
|
|
12
|
+
[:per_page!, :limit!, :offset!, :order!, :preserve_order!].each do |p|
|
|
13
|
+
instance_variable_set("@#{p.to_s.gsub(/!/, '')}", parameters[p])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
handle_pagination if per_page
|
|
17
|
+
convert_ids_to_uuids
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def preserve_order?
|
|
21
|
+
preserve_order
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def offset?
|
|
25
|
+
offset
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
def convert_ids_to_uuids
|
|
30
|
+
if conditions[:id] && conditions[:id].is_a?(Array)
|
|
31
|
+
conditions[:id] = conditions[:id].map { |i| uuid_klass.new(i) }
|
|
32
|
+
elsif conditions[:id]
|
|
33
|
+
conditions[:id] = uuid_klass.new(conditions[:id])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_pagination
|
|
38
|
+
@limit = per_page
|
|
39
|
+
@offset = (page - 1) * per_page
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'sequel'
|
|
2
|
+
|
|
3
|
+
# Out of the box, Sequel uses IS TRUE/FALSE for boolean parameters
|
|
4
|
+
# This prevents MySQL from using indexes.
|
|
5
|
+
#
|
|
6
|
+
# This patch fixes that.
|
|
7
|
+
module Sequel
|
|
8
|
+
module SQL
|
|
9
|
+
class BooleanExpression
|
|
10
|
+
def self.from_value_pairs(pairs, op=:AND, negate=false)
|
|
11
|
+
pairs = pairs.collect do |l,r|
|
|
12
|
+
ce = case r
|
|
13
|
+
when Range
|
|
14
|
+
new(:AND, new(:>=, l, r.begin), new(r.exclude_end? ? :< : :<=, l, r.end))
|
|
15
|
+
when Array, ::Sequel::Dataset, SQLArray
|
|
16
|
+
new(:IN, l, r)
|
|
17
|
+
when NegativeBooleanConstant
|
|
18
|
+
new(:"IS NOT", l, r.constant)
|
|
19
|
+
when BooleanConstant
|
|
20
|
+
new(:IS, l, r.constant)
|
|
21
|
+
when NilClass
|
|
22
|
+
new(:IS, l, r)
|
|
23
|
+
when Regexp
|
|
24
|
+
StringExpression.like(l, r)
|
|
25
|
+
else
|
|
26
|
+
new(:'=', l, r)
|
|
27
|
+
end
|
|
28
|
+
negate ? invert(ce) : ce
|
|
29
|
+
end
|
|
30
|
+
pairs.length == 1 ? pairs.at(0) : new(op, *pairs)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class Storage
|
|
3
|
+
def create(document)
|
|
4
|
+
raise NotImplementedError, "#{self.class.name}#create is not implemented."
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def update(document)
|
|
8
|
+
raise NotImplementedError, "#{self.class.name}#update is not implemented."
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def destroy(document)
|
|
12
|
+
raise NotImplementedError, "#{self.class.name}#destroy is not implemented."
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def first(conditions)
|
|
16
|
+
raise NotImplementedError, "#{self.class.name}#first is not implemented."
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def all(conditions)
|
|
20
|
+
raise NotImplementedError, "#{self.class.name}#all is not implemented."
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def count(query)
|
|
24
|
+
raise NotImplementedError, "#{self.class.name}#count is not implemented."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def satisfies?(conditions)
|
|
28
|
+
raise NotImplementedError, "#{self.class.name}#satisfies? is not implemented."
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class StorageFactory
|
|
3
|
+
attr_reader :table_klass, :index_klass, :cache_klass
|
|
4
|
+
|
|
5
|
+
def initialize(table_klass = DocumentTable, index_klass = Index,
|
|
6
|
+
cache_klass = Cache)
|
|
7
|
+
@table_klass = table_klass
|
|
8
|
+
@index_klass = index_klass
|
|
9
|
+
@cache_klass = cache_klass
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def document_table(*args)
|
|
13
|
+
table_klass.new(*args)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def index(*args)
|
|
17
|
+
index_klass.new(*args)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def cache(*args)
|
|
21
|
+
cache_klass.cache_for(*args)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require 'friendly/storage_factory'
|
|
2
|
+
require 'friendly/table_creator'
|
|
3
|
+
|
|
4
|
+
module Friendly
|
|
5
|
+
class StorageProxy
|
|
6
|
+
attr_reader :klass, :storage_factory, :tables, :table_creator, :caches
|
|
7
|
+
|
|
8
|
+
def initialize(klass, storage_factory = StorageFactory.new,
|
|
9
|
+
table_creator=TableCreator.new)
|
|
10
|
+
super()
|
|
11
|
+
@klass = klass
|
|
12
|
+
@storage_factory = storage_factory
|
|
13
|
+
@table_creator = table_creator
|
|
14
|
+
@tables = [storage_factory.document_table(klass)]
|
|
15
|
+
@caches = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def first(conditions)
|
|
19
|
+
first_from_cache(conditions) do
|
|
20
|
+
index_for(conditions).first(conditions)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def all(query)
|
|
25
|
+
objects = perform_all(query).compact
|
|
26
|
+
if query.preserve_order?
|
|
27
|
+
order = query.conditions[:id]
|
|
28
|
+
objects.sort { |a,b| order.index(a.id) <=> order.index(b.id) }
|
|
29
|
+
else
|
|
30
|
+
objects
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def count(query)
|
|
35
|
+
index_for(query).count(query)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add(*args)
|
|
39
|
+
tables << storage_factory.index(klass, *args)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cache(fields, options = {})
|
|
43
|
+
caches << storage_factory.cache(klass, fields, options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def create(document)
|
|
47
|
+
each_store { |s| s.create(document) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def update(document)
|
|
51
|
+
each_store { |s| s.update(document) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def destroy(document)
|
|
55
|
+
stores.reverse.each { |i| i.destroy(document) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def create_tables!
|
|
59
|
+
tables.each { |t| table_creator.create(t) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def index_for(conditions)
|
|
63
|
+
index = tables.detect { |i| i.satisfies?(conditions) }
|
|
64
|
+
if index.nil?
|
|
65
|
+
raise MissingIndex, "No index found to satisfy: #{conditions.inspect}."
|
|
66
|
+
end
|
|
67
|
+
index
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
protected
|
|
71
|
+
def each_store
|
|
72
|
+
stores.each { |s| yield(s) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def stores
|
|
76
|
+
tables + caches
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def first_from_cache(query)
|
|
80
|
+
cache = cache_for(query)
|
|
81
|
+
if cache
|
|
82
|
+
cache.first(query) { yield }
|
|
83
|
+
else
|
|
84
|
+
yield
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cache_for(query)
|
|
89
|
+
caches.detect { |c| c.satisfies?(query) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def perform_all(query)
|
|
93
|
+
cache = cache_for(query)
|
|
94
|
+
if cache
|
|
95
|
+
cache.all(query) do |missing_key|
|
|
96
|
+
index_for(query).first(Query.new(:id => missing_key.split("/").last))
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
index_for(query).all(query)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'friendly/storage'
|
|
2
|
+
module Friendly
|
|
3
|
+
class Table < Storage
|
|
4
|
+
attr_reader :datastore
|
|
5
|
+
|
|
6
|
+
def initialize(datastore)
|
|
7
|
+
@datastore = datastore
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def table_name
|
|
11
|
+
raise NotImplementedError, "#{self.class.name}#table_name is not implemented."
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class TableCreator
|
|
3
|
+
attr_reader :db
|
|
4
|
+
|
|
5
|
+
def initialize(db = Friendly.db)
|
|
6
|
+
@db = db
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create(table)
|
|
10
|
+
unless db.table_exists?(table.table_name)
|
|
11
|
+
case table
|
|
12
|
+
when DocumentTable
|
|
13
|
+
create_document_table(table)
|
|
14
|
+
when Index
|
|
15
|
+
create_index_table(table)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
def create_document_table(table)
|
|
22
|
+
db.create_table(table.table_name) do
|
|
23
|
+
primary_key :added_id
|
|
24
|
+
binary :id, :size => 16
|
|
25
|
+
String :attributes, :text => true
|
|
26
|
+
Time :created_at
|
|
27
|
+
Time :updated_at
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def create_index_table(table)
|
|
32
|
+
db.create_table(table.table_name) do
|
|
33
|
+
binary :id, :size => 16
|
|
34
|
+
table.fields.flatten.each do |f|
|
|
35
|
+
method(table.klass.attributes[f].type.name.to_sym).call(f)
|
|
36
|
+
end
|
|
37
|
+
primary_key table.fields.flatten + [:id]
|
|
38
|
+
unique :id
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# This class was extracted from the cassandra gem by Evan Weaver
|
|
2
|
+
# As such, it is distributed under the terms of the apache license.
|
|
3
|
+
# See the APACHE-LICENSE file in the root of this project for more information.
|
|
4
|
+
#
|
|
5
|
+
class Time
|
|
6
|
+
def self.stamp
|
|
7
|
+
Time.now.stamp
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def stamp
|
|
11
|
+
to_i * 1_000_000 + usec
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Friendly
|
|
2
|
+
class Translator
|
|
3
|
+
RESERVED_ATTRS = [:id, :created_at, :updated_at].freeze
|
|
4
|
+
|
|
5
|
+
attr_reader :serializer, :time
|
|
6
|
+
|
|
7
|
+
def initialize(serializer = JSON, time = Time)
|
|
8
|
+
@serializer = serializer
|
|
9
|
+
@time = time
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_object(klass, record)
|
|
13
|
+
record.delete(:added_id)
|
|
14
|
+
attributes = serializer.parse(record.delete(:attributes))
|
|
15
|
+
klass.new attributes.merge(record).merge(:new_record => false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_record(document)
|
|
19
|
+
{ :id => document.id,
|
|
20
|
+
:created_at => document.created_at,
|
|
21
|
+
:updated_at => time.new,
|
|
22
|
+
:attributes => serialize(document) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
def serialize(document)
|
|
27
|
+
attrs = document.to_hash.reject { |k,v| RESERVED_ATTRS.include?(k) }
|
|
28
|
+
serializer.generate(attrs)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
require 'friendly/time'
|
|
2
|
+
# This class was extracted from the cassandra gem by Evan Weaver
|
|
3
|
+
# As such, it is distributed under the terms of the apache license.
|
|
4
|
+
# See the APACHE-LICENSE file in the root of this project for more information.
|
|
5
|
+
#
|
|
6
|
+
module Friendly
|
|
7
|
+
# UUID format version 1, as specified in RFC 4122, with jitter in place of the mac address and sequence counter.
|
|
8
|
+
class UUID
|
|
9
|
+
|
|
10
|
+
class InvalidVersion < StandardError; end
|
|
11
|
+
class TypeError < ::TypeError; end
|
|
12
|
+
|
|
13
|
+
GREGORIAN_EPOCH_OFFSET = 0x01B2_1DD2_1381_4000 # Oct 15, 1582
|
|
14
|
+
|
|
15
|
+
VARIANT = 0b1000_0000_0000_0000
|
|
16
|
+
|
|
17
|
+
def initialize(bytes = nil)
|
|
18
|
+
case bytes
|
|
19
|
+
when self.class # UUID
|
|
20
|
+
@bytes = bytes.to_s
|
|
21
|
+
when String
|
|
22
|
+
case bytes.size
|
|
23
|
+
when 16 # Raw byte array
|
|
24
|
+
@bytes = bytes
|
|
25
|
+
when 36 # Human-readable UUID representation; inverse of #to_guid
|
|
26
|
+
elements = bytes.split("-")
|
|
27
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (malformed UUID representation)" if elements.size != 5
|
|
28
|
+
@bytes = elements.join.to_a.pack('H32')
|
|
29
|
+
else
|
|
30
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (invalid bytecount)"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
when Integer
|
|
34
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (integer out of range)" if bytes < 0 or bytes > 2**128
|
|
35
|
+
@bytes = [
|
|
36
|
+
(bytes >> 96) & 0xFFFF_FFFF,
|
|
37
|
+
(bytes >> 64) & 0xFFFF_FFFF,
|
|
38
|
+
(bytes >> 32) & 0xFFFF_FFFF,
|
|
39
|
+
bytes & 0xFFFF_FFFF
|
|
40
|
+
].pack("NNNN")
|
|
41
|
+
|
|
42
|
+
when NilClass, Time
|
|
43
|
+
time = (bytes || Time).stamp * 10 + GREGORIAN_EPOCH_OFFSET
|
|
44
|
+
# See http://github.com/spectra/ruby-uuid/
|
|
45
|
+
@bytes = [
|
|
46
|
+
time & 0xFFFF_FFFF,
|
|
47
|
+
time >> 32,
|
|
48
|
+
((time >> 48) & 0x0FFF) | 0x1000,
|
|
49
|
+
# Top 3 bytes reserved
|
|
50
|
+
rand(2**13) | VARIANT,
|
|
51
|
+
rand(2**16),
|
|
52
|
+
rand(2**32)
|
|
53
|
+
].pack("NnnnnN")
|
|
54
|
+
|
|
55
|
+
else
|
|
56
|
+
raise TypeError, "Expected #{bytes.inspect} to cast to a #{self.class} (unknown source class)"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_i
|
|
61
|
+
ints = @bytes.unpack("NNNN")
|
|
62
|
+
(ints[0] << 96) +
|
|
63
|
+
(ints[1] << 64) +
|
|
64
|
+
(ints[2] << 32) +
|
|
65
|
+
ints[3]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def version
|
|
69
|
+
time_high = @bytes.unpack("NnnQ")[2]
|
|
70
|
+
version = (time_high & 0xF000).to_s(16)[0].chr.to_i
|
|
71
|
+
version > 0 and version < 6 ? version : -1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def variant
|
|
75
|
+
@bytes.unpack('QnnN')[1] >> 13
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def to_guid
|
|
79
|
+
elements = @bytes.unpack("NnnCCa6")
|
|
80
|
+
node = elements[-1].unpack('C*')
|
|
81
|
+
elements[-1] = '%02x%02x%02x%02x%02x%02x' % node
|
|
82
|
+
"%08x-%04x-%04x-%02x%02x-%s" % elements
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def to_json(*args)
|
|
86
|
+
to_guid.to_json(*args)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def seconds
|
|
90
|
+
total_usecs / 1_000_000
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def usecs
|
|
94
|
+
total_usecs % 1_000_000
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def <=>(other)
|
|
98
|
+
total_usecs <=> other.send(:total_usecs)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def inspect(long = false)
|
|
102
|
+
"<Friendly::UUID##{object_id} time: #{
|
|
103
|
+
Time.at(seconds).inspect
|
|
104
|
+
}, usecs: #{
|
|
105
|
+
usecs
|
|
106
|
+
} jitter: #{
|
|
107
|
+
@bytes.unpack('QQ')[1]
|
|
108
|
+
}" + (long ? ", version: #{version}, variant: #{variant}, guid: #{to_guid}>" : ">")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def <=>(other)
|
|
112
|
+
self.to_i <=> other.to_i
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def hash
|
|
116
|
+
@bytes.hash
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def eql?(other)
|
|
120
|
+
other.is_a?(Comparable) and @bytes == other.to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ==(other)
|
|
124
|
+
self.to_s == other.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_s
|
|
128
|
+
@bytes
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def sql_literal(dataset)
|
|
132
|
+
dataset.literal(to_s)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def total_usecs
|
|
138
|
+
elements = @bytes.unpack("NnnQ")
|
|
139
|
+
(elements[0] + (elements[1] << 32) + ((elements[2] & 0x0FFF) << 48) - GREGORIAN_EPOCH_OFFSET) / 10
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
data/lib/friendly.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'friendly/attribute'
|
|
2
|
+
require 'friendly/boolean'
|
|
3
|
+
require 'friendly/cache'
|
|
4
|
+
require 'friendly/cache/by_id'
|
|
5
|
+
require 'friendly/config'
|
|
6
|
+
require 'friendly/data_store'
|
|
7
|
+
require 'friendly/document'
|
|
8
|
+
require 'friendly/document_table'
|
|
9
|
+
require 'friendly/index'
|
|
10
|
+
require 'friendly/memcached'
|
|
11
|
+
require 'friendly/query'
|
|
12
|
+
require 'friendly/sequel_monkey_patches'
|
|
13
|
+
require 'friendly/storage_factory'
|
|
14
|
+
require 'friendly/storage_proxy'
|
|
15
|
+
require 'friendly/translator'
|
|
16
|
+
require 'friendly/uuid'
|
|
17
|
+
|
|
18
|
+
require 'will_paginate/collection'
|
|
19
|
+
|
|
20
|
+
module Friendly
|
|
21
|
+
class << self
|
|
22
|
+
attr_accessor :datastore, :db, :cache
|
|
23
|
+
|
|
24
|
+
def configure(config)
|
|
25
|
+
self.db = Sequel.connect(config)
|
|
26
|
+
self.datastore = DataStore.new(db)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def batch
|
|
30
|
+
begin
|
|
31
|
+
datastore.start_batch
|
|
32
|
+
yield
|
|
33
|
+
datastore.flush_batch
|
|
34
|
+
ensure
|
|
35
|
+
datastore.reset_batch
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_tables!
|
|
40
|
+
Document.create_tables!
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Error < RuntimeError; end
|
|
45
|
+
class RecordNotFound < Error; end
|
|
46
|
+
class MissingIndex < Error; end
|
|
47
|
+
class NoConverterExists < Friendly::Error; end
|
|
48
|
+
class NotSupported < Friendly::Error; end
|
|
49
|
+
end
|
data/rails/init.rb
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class DataStoreFake
|
|
2
|
+
attr_writer :insert, :all, :first
|
|
3
|
+
attr_reader :inserts, :updates
|
|
4
|
+
|
|
5
|
+
def initialize(opts = {})
|
|
6
|
+
opts.each { |k,v| send("#{k}=", v) }
|
|
7
|
+
@inserts = []
|
|
8
|
+
@updates = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def insert(*args)
|
|
12
|
+
@inserts << args
|
|
13
|
+
@insert
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def update(*args)
|
|
17
|
+
@updates << args
|
|
18
|
+
@update
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def all(*args)
|
|
22
|
+
@all[args]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def first(*args)
|
|
26
|
+
@first[args]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|