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.
Files changed (110) hide show
  1. data/.document +5 -0
  2. data/.gitignore +23 -0
  3. data/APACHE-LICENSE +202 -0
  4. data/LICENSE +20 -0
  5. data/README.md +173 -0
  6. data/Rakefile +67 -0
  7. data/VERSION +1 -0
  8. data/examples/friendly.yml +7 -0
  9. data/friendly.gemspec +201 -0
  10. data/lib/friendly/attribute.rb +65 -0
  11. data/lib/friendly/boolean.rb +6 -0
  12. data/lib/friendly/cache/by_id.rb +33 -0
  13. data/lib/friendly/cache.rb +24 -0
  14. data/lib/friendly/config.rb +5 -0
  15. data/lib/friendly/data_store.rb +72 -0
  16. data/lib/friendly/document.rb +165 -0
  17. data/lib/friendly/document_table.rb +56 -0
  18. data/lib/friendly/index.rb +73 -0
  19. data/lib/friendly/memcached.rb +48 -0
  20. data/lib/friendly/newrelic.rb +6 -0
  21. data/lib/friendly/query.rb +42 -0
  22. data/lib/friendly/sequel_monkey_patches.rb +35 -0
  23. data/lib/friendly/storage.rb +31 -0
  24. data/lib/friendly/storage_factory.rb +24 -0
  25. data/lib/friendly/storage_proxy.rb +103 -0
  26. data/lib/friendly/table.rb +15 -0
  27. data/lib/friendly/table_creator.rb +43 -0
  28. data/lib/friendly/time.rb +14 -0
  29. data/lib/friendly/translator.rb +32 -0
  30. data/lib/friendly/uuid.rb +143 -0
  31. data/lib/friendly.rb +49 -0
  32. data/rails/init.rb +3 -0
  33. data/spec/fakes/data_store_fake.rb +29 -0
  34. data/spec/fakes/database_fake.rb +12 -0
  35. data/spec/fakes/dataset_fake.rb +28 -0
  36. data/spec/fakes/document.rb +18 -0
  37. data/spec/fakes/serializer_fake.rb +12 -0
  38. data/spec/fakes/time_fake.rb +12 -0
  39. data/spec/integration/basic_object_lifecycle_spec.rb +114 -0
  40. data/spec/integration/batch_insertion_spec.rb +29 -0
  41. data/spec/integration/convenience_api_spec.rb +25 -0
  42. data/spec/integration/count_spec.rb +12 -0
  43. data/spec/integration/default_value_spec.rb +15 -0
  44. data/spec/integration/find_via_cache_spec.rb +101 -0
  45. data/spec/integration/finder_spec.rb +64 -0
  46. data/spec/integration/index_spec.rb +57 -0
  47. data/spec/integration/pagination_spec.rb +63 -0
  48. data/spec/integration/table_creator_spec.rb +52 -0
  49. data/spec/integration/write_through_cache_spec.rb +53 -0
  50. data/spec/spec.opts +1 -0
  51. data/spec/spec_helper.rb +90 -0
  52. data/spec/unit/attribute_spec.rb +64 -0
  53. data/spec/unit/cache_by_id_spec.rb +102 -0
  54. data/spec/unit/cache_spec.rb +21 -0
  55. data/spec/unit/config_spec.rb +4 -0
  56. data/spec/unit/data_store_spec.rb +188 -0
  57. data/spec/unit/document_spec.rb +311 -0
  58. data/spec/unit/document_table_spec.rb +126 -0
  59. data/spec/unit/friendly_spec.rb +25 -0
  60. data/spec/unit/index_spec.rb +196 -0
  61. data/spec/unit/memcached_spec.rb +114 -0
  62. data/spec/unit/query_spec.rb +104 -0
  63. data/spec/unit/storage_factory_spec.rb +59 -0
  64. data/spec/unit/storage_proxy_spec.rb +218 -0
  65. data/spec/unit/translator_spec.rb +96 -0
  66. data/website/index.html +210 -0
  67. data/website/scripts/clipboard.swf +0 -0
  68. data/website/scripts/shBrushAS3.js +61 -0
  69. data/website/scripts/shBrushBash.js +66 -0
  70. data/website/scripts/shBrushCSharp.js +67 -0
  71. data/website/scripts/shBrushColdFusion.js +102 -0
  72. data/website/scripts/shBrushCpp.js +99 -0
  73. data/website/scripts/shBrushCss.js +93 -0
  74. data/website/scripts/shBrushDelphi.js +57 -0
  75. data/website/scripts/shBrushDiff.js +43 -0
  76. data/website/scripts/shBrushErlang.js +54 -0
  77. data/website/scripts/shBrushGroovy.js +69 -0
  78. data/website/scripts/shBrushJScript.js +52 -0
  79. data/website/scripts/shBrushJava.js +59 -0
  80. data/website/scripts/shBrushJavaFX.js +60 -0
  81. data/website/scripts/shBrushPerl.js +74 -0
  82. data/website/scripts/shBrushPhp.js +91 -0
  83. data/website/scripts/shBrushPlain.js +35 -0
  84. data/website/scripts/shBrushPowerShell.js +76 -0
  85. data/website/scripts/shBrushPython.js +66 -0
  86. data/website/scripts/shBrushRuby.js +57 -0
  87. data/website/scripts/shBrushScala.js +53 -0
  88. data/website/scripts/shBrushSql.js +68 -0
  89. data/website/scripts/shBrushVb.js +58 -0
  90. data/website/scripts/shBrushXml.js +71 -0
  91. data/website/scripts/shCore.js +30 -0
  92. data/website/scripts/shLegacy.js +30 -0
  93. data/website/styles/friendly.css +103 -0
  94. data/website/styles/help.png +0 -0
  95. data/website/styles/ie.css +35 -0
  96. data/website/styles/magnifier.png +0 -0
  97. data/website/styles/page_white_code.png +0 -0
  98. data/website/styles/page_white_copy.png +0 -0
  99. data/website/styles/print.css +29 -0
  100. data/website/styles/printer.png +0 -0
  101. data/website/styles/screen.css +257 -0
  102. data/website/styles/shCore.css +330 -0
  103. data/website/styles/shThemeDefault.css +173 -0
  104. data/website/styles/shThemeDjango.css +176 -0
  105. data/website/styles/shThemeEclipse.css +190 -0
  106. data/website/styles/shThemeEmacs.css +175 -0
  107. data/website/styles/shThemeFadeToGrey.css +177 -0
  108. data/website/styles/shThemeMidnight.css +175 -0
  109. data/website/styles/shThemeRDark.css +175 -0
  110. 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,6 @@
1
+ Friendly::Memcached.class_eval do
2
+ add_method_tracer :miss, "Friendly::Memcached/miss"
3
+ add_method_tracer :get, "Friendly::Memcached/get"
4
+ add_method_tracer :multiget, "Friendly::Memcached/multiget"
5
+ end
6
+
@@ -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,3 @@
1
+ config = YAML.load(File.read(RAILS_ROOT + "/config/friendly.yml"))[RAILS_ENV]
2
+ Friendly.configure(config)
3
+
@@ -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
+