wayne-friendly 0.5.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.
Files changed (136) hide show
  1. data/.document +2 -0
  2. data/.gitignore +26 -0
  3. data/APACHE-LICENSE +202 -0
  4. data/CHANGELOG.md +28 -0
  5. data/CONTRIBUTORS.md +7 -0
  6. data/LICENSE +20 -0
  7. data/README.md +288 -0
  8. data/Rakefile +68 -0
  9. data/TODO.md +5 -0
  10. data/VERSION +1 -0
  11. data/examples/friendly.yml +7 -0
  12. data/friendly.gemspec +240 -0
  13. data/lib/friendly.rb +58 -0
  14. data/lib/friendly/associations.rb +7 -0
  15. data/lib/friendly/associations/association.rb +34 -0
  16. data/lib/friendly/associations/set.rb +37 -0
  17. data/lib/friendly/attribute.rb +98 -0
  18. data/lib/friendly/boolean.rb +10 -0
  19. data/lib/friendly/cache.rb +24 -0
  20. data/lib/friendly/cache/by_id.rb +33 -0
  21. data/lib/friendly/data_store.rb +73 -0
  22. data/lib/friendly/document.rb +70 -0
  23. data/lib/friendly/document/associations.rb +50 -0
  24. data/lib/friendly/document/attributes.rb +114 -0
  25. data/lib/friendly/document/convenience.rb +41 -0
  26. data/lib/friendly/document/mixin.rb +15 -0
  27. data/lib/friendly/document/scoping.rb +66 -0
  28. data/lib/friendly/document/storage.rb +63 -0
  29. data/lib/friendly/document_table.rb +56 -0
  30. data/lib/friendly/index.rb +73 -0
  31. data/lib/friendly/indexer.rb +50 -0
  32. data/lib/friendly/memcached.rb +48 -0
  33. data/lib/friendly/newrelic.rb +6 -0
  34. data/lib/friendly/query.rb +42 -0
  35. data/lib/friendly/scope.rb +100 -0
  36. data/lib/friendly/scope_proxy.rb +43 -0
  37. data/lib/friendly/sequel_monkey_patches.rb +34 -0
  38. data/lib/friendly/storage.rb +31 -0
  39. data/lib/friendly/storage_factory.rb +24 -0
  40. data/lib/friendly/storage_proxy.rb +111 -0
  41. data/lib/friendly/table.rb +15 -0
  42. data/lib/friendly/table_creator.rb +50 -0
  43. data/lib/friendly/time.rb +14 -0
  44. data/lib/friendly/translator.rb +33 -0
  45. data/lib/friendly/uuid.rb +148 -0
  46. data/lib/tasks/friendly.rake +7 -0
  47. data/rails/init.rb +3 -0
  48. data/spec/config.yml.example +7 -0
  49. data/spec/fakes/data_store_fake.rb +29 -0
  50. data/spec/fakes/database_fake.rb +12 -0
  51. data/spec/fakes/dataset_fake.rb +28 -0
  52. data/spec/fakes/document.rb +18 -0
  53. data/spec/fakes/serializer_fake.rb +12 -0
  54. data/spec/fakes/time_fake.rb +12 -0
  55. data/spec/integration/ad_hoc_scopes_spec.rb +42 -0
  56. data/spec/integration/basic_object_lifecycle_spec.rb +114 -0
  57. data/spec/integration/batch_insertion_spec.rb +29 -0
  58. data/spec/integration/convenience_api_spec.rb +25 -0
  59. data/spec/integration/count_spec.rb +12 -0
  60. data/spec/integration/default_value_spec.rb +30 -0
  61. data/spec/integration/dirty_tracking_spec.rb +43 -0
  62. data/spec/integration/find_via_cache_spec.rb +101 -0
  63. data/spec/integration/finder_spec.rb +71 -0
  64. data/spec/integration/has_many_spec.rb +18 -0
  65. data/spec/integration/index_spec.rb +57 -0
  66. data/spec/integration/named_scope_spec.rb +34 -0
  67. data/spec/integration/offline_indexing_spec.rb +53 -0
  68. data/spec/integration/pagination_spec.rb +63 -0
  69. data/spec/integration/scope_chaining_spec.rb +22 -0
  70. data/spec/integration/table_creator_spec.rb +69 -0
  71. data/spec/integration/write_through_cache_spec.rb +53 -0
  72. data/spec/spec.opts +1 -0
  73. data/spec/spec_helper.rb +105 -0
  74. data/spec/unit/associations/association_spec.rb +57 -0
  75. data/spec/unit/associations/set_spec.rb +43 -0
  76. data/spec/unit/attribute_spec.rb +125 -0
  77. data/spec/unit/cache_by_id_spec.rb +102 -0
  78. data/spec/unit/cache_spec.rb +21 -0
  79. data/spec/unit/data_store_spec.rb +201 -0
  80. data/spec/unit/document/attributes_spec.rb +130 -0
  81. data/spec/unit/document_spec.rb +318 -0
  82. data/spec/unit/document_table_spec.rb +126 -0
  83. data/spec/unit/friendly_spec.rb +25 -0
  84. data/spec/unit/index_spec.rb +196 -0
  85. data/spec/unit/memcached_spec.rb +114 -0
  86. data/spec/unit/query_spec.rb +104 -0
  87. data/spec/unit/scope_proxy_spec.rb +44 -0
  88. data/spec/unit/scope_spec.rb +113 -0
  89. data/spec/unit/storage_factory_spec.rb +59 -0
  90. data/spec/unit/storage_proxy_spec.rb +244 -0
  91. data/spec/unit/translator_spec.rb +91 -0
  92. data/website/index.html +210 -0
  93. data/website/scripts/clipboard.swf +0 -0
  94. data/website/scripts/shBrushAS3.js +61 -0
  95. data/website/scripts/shBrushBash.js +66 -0
  96. data/website/scripts/shBrushCSharp.js +67 -0
  97. data/website/scripts/shBrushColdFusion.js +102 -0
  98. data/website/scripts/shBrushCpp.js +99 -0
  99. data/website/scripts/shBrushCss.js +93 -0
  100. data/website/scripts/shBrushDelphi.js +57 -0
  101. data/website/scripts/shBrushDiff.js +43 -0
  102. data/website/scripts/shBrushErlang.js +54 -0
  103. data/website/scripts/shBrushGroovy.js +69 -0
  104. data/website/scripts/shBrushJScript.js +52 -0
  105. data/website/scripts/shBrushJava.js +59 -0
  106. data/website/scripts/shBrushJavaFX.js +60 -0
  107. data/website/scripts/shBrushPerl.js +74 -0
  108. data/website/scripts/shBrushPhp.js +91 -0
  109. data/website/scripts/shBrushPlain.js +35 -0
  110. data/website/scripts/shBrushPowerShell.js +76 -0
  111. data/website/scripts/shBrushPython.js +66 -0
  112. data/website/scripts/shBrushRuby.js +57 -0
  113. data/website/scripts/shBrushScala.js +53 -0
  114. data/website/scripts/shBrushSql.js +68 -0
  115. data/website/scripts/shBrushVb.js +58 -0
  116. data/website/scripts/shBrushXml.js +71 -0
  117. data/website/scripts/shCore.js +30 -0
  118. data/website/scripts/shLegacy.js +30 -0
  119. data/website/styles/friendly.css +103 -0
  120. data/website/styles/help.png +0 -0
  121. data/website/styles/ie.css +35 -0
  122. data/website/styles/magnifier.png +0 -0
  123. data/website/styles/page_white_code.png +0 -0
  124. data/website/styles/page_white_copy.png +0 -0
  125. data/website/styles/print.css +29 -0
  126. data/website/styles/printer.png +0 -0
  127. data/website/styles/screen.css +257 -0
  128. data/website/styles/shCore.css +330 -0
  129. data/website/styles/shThemeDefault.css +173 -0
  130. data/website/styles/shThemeDjango.css +176 -0
  131. data/website/styles/shThemeEclipse.css +190 -0
  132. data/website/styles/shThemeEmacs.css +175 -0
  133. data/website/styles/shThemeFadeToGrey.css +177 -0
  134. data/website/styles/shThemeMidnight.css +175 -0
  135. data/website/styles/shThemeRDark.css +175 -0
  136. metadata +337 -0
@@ -0,0 +1,100 @@
1
+ module Friendly
2
+ class Scope
3
+ attr_reader :klass, :parameters
4
+
5
+ def initialize(klass, parameters)
6
+ @klass = klass
7
+ @parameters = parameters
8
+ end
9
+
10
+ # Fetch all documents at this scope.
11
+ #
12
+ # @param [Hash] extra_parameters add extra parameters to this query.
13
+ #
14
+ def all(extra_parameters = {})
15
+ klass.all(params(extra_parameters))
16
+ end
17
+
18
+ # Fetch the first document at this scope.
19
+ #
20
+ # @param [Hash] extra_parameters add extra parameters to this query.
21
+ #
22
+ def first(extra_parameters = {})
23
+ klass.first(params(extra_parameters))
24
+ end
25
+
26
+ # Paginate the documents at this scope.
27
+ #
28
+ # @param [Hash] extra_parameters add extra parameters to this query.
29
+ # @return WillPaginate::Collection
30
+ #
31
+ def paginate(extra_parameters = {})
32
+ klass.paginate(params(extra_parameters))
33
+ end
34
+
35
+ # Build an object at this scope.
36
+ #
37
+ # e.g.
38
+ # Post.scope(:name => "James").build.name # => "James"
39
+ #
40
+ # @param [Hash] extra_parameters add extra parameters to this query.
41
+ #
42
+ def build(extra_parameters = {})
43
+ klass.new(params_without_modifiers(extra_parameters))
44
+ end
45
+
46
+ # Create an object at this scope.
47
+ #
48
+ # e.g.
49
+ # @post = Post.scope(:name => "James").create
50
+ # @post.new_record? # => false
51
+ # @post.name # => "James"
52
+ #
53
+ # @param [Hash] extra_parameters add extra parameters to this query.
54
+ #
55
+ def create(extra_parameters = {})
56
+ klass.create(params_without_modifiers(extra_parameters))
57
+ end
58
+
59
+ # Override #respond_to? so that we can return true when it's another named_scope.
60
+ #
61
+ # @override
62
+ #
63
+ def respond_to?(method_name, include_private = false)
64
+ klass.has_named_scope?(method_name) || super
65
+ end
66
+
67
+ # Use method_missing to respond to other named scopes on klass.
68
+ #
69
+ # @override
70
+ #
71
+ def method_missing(method_name, *args, &block)
72
+ respond_to?(method_name) ? chain_with(method_name) : super
73
+ end
74
+
75
+ # Chain with another one of klass's named_scopes.
76
+ #
77
+ # @param [Symbol] scope_name The name of the scope to chain with.
78
+ #
79
+ def chain_with(scope_name)
80
+ self + klass.send(scope_name)
81
+ end
82
+
83
+ # Create a new Scope that is the combination of self and other, where other takes priority
84
+ #
85
+ # @param [Friendly::Scope] other The scope to merge with.
86
+ #
87
+ def +(other_scope)
88
+ self.class.new(klass, parameters.merge(other_scope.parameters))
89
+ end
90
+
91
+ protected
92
+ def params(extra)
93
+ parameters.merge(extra)
94
+ end
95
+
96
+ def params_without_modifiers(extra)
97
+ params(extra).reject { |k,v| k.to_s =~ /!$/ }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,43 @@
1
+ module Friendly
2
+ class ScopeProxy
3
+ attr_reader :klass, :scope_klass, :scopes
4
+
5
+ def initialize(klass, scope_klass = Scope)
6
+ @klass = klass
7
+ @scope_klass = scope_klass
8
+ @scopes = {}
9
+ end
10
+
11
+ def add_named(name, parameters)
12
+ scopes[name] = parameters
13
+ add_scope_method_to_klass(name)
14
+ end
15
+
16
+ def get(name)
17
+ scopes[name]
18
+ end
19
+
20
+ def get_instance(name)
21
+ scope_klass.new(klass, get(name))
22
+ end
23
+
24
+ def ad_hoc(parameters)
25
+ scope_klass.new(klass, parameters)
26
+ end
27
+
28
+ def has_named_scope?(name)
29
+ scopes.has_key?(name)
30
+ end
31
+
32
+ protected
33
+ def add_scope_method_to_klass(scope_name)
34
+ klass.class_eval do
35
+ eval <<-__END__
36
+ def self.#{scope_name}
37
+ scope_proxy.get_instance(:#{scope_name})
38
+ end
39
+ __END__
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,34 @@
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
@@ -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,111 @@
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
+ def index_for_fields(fields)
71
+ tables.detect do |t|
72
+ t.respond_to?(:fields) && t.fields == fields
73
+ end.tap do |i|
74
+ raise MissingIndex, "No index found matching #{fields.join(", ")}." if i.nil?
75
+ end
76
+ end
77
+
78
+ protected
79
+ def each_store
80
+ stores.each { |s| yield(s) }
81
+ end
82
+
83
+ def stores
84
+ tables + caches
85
+ end
86
+
87
+ def first_from_cache(query)
88
+ cache = cache_for(query)
89
+ if cache
90
+ cache.first(query) { yield }
91
+ else
92
+ yield
93
+ end
94
+ end
95
+
96
+ def cache_for(query)
97
+ caches.detect { |c| c.satisfies?(query) }
98
+ end
99
+
100
+ def perform_all(query)
101
+ cache = cache_for(query)
102
+ if cache
103
+ cache.all(query) do |missing_key|
104
+ index_for(query).first(Query.new(:id => missing_key.split("/").last))
105
+ end
106
+ else
107
+ index_for(query).all(query)
108
+ end
109
+ end
110
+ end
111
+ 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,50 @@
1
+ module Friendly
2
+ class TableCreator
3
+ attr_reader :db, :attr_klass
4
+
5
+ def initialize(db = Friendly.db, attr_klass = Friendly::Attribute)
6
+ @db = db
7
+ @attr_klass = attr_klass
8
+ end
9
+
10
+ def create(table)
11
+ unless db.table_exists?(table.table_name)
12
+ case table
13
+ when DocumentTable
14
+ create_document_table(table)
15
+ when Index
16
+ create_index_table(table)
17
+ end
18
+ end
19
+ end
20
+
21
+ protected
22
+ def create_document_table(table)
23
+ db.create_table(table.table_name) do
24
+ primary_key :added_id
25
+ binary :id, :size => 16
26
+ String :attributes, :text => true
27
+ Time :created_at
28
+ Time :updated_at
29
+
30
+ unique :id
31
+ end
32
+ end
33
+
34
+ def create_index_table(table)
35
+ attr = attr_klass # close around this please
36
+
37
+ db.create_table(table.table_name) do
38
+ binary :id, :size => 16
39
+ table.fields.flatten.each do |f|
40
+ klass = table.klass.attributes[f].type
41
+ type = attr.custom_type?(klass) ? attr.sql_type(klass) : klass
42
+ column(f, type)
43
+ end
44
+ primary_key table.fields.flatten + [:id]
45
+ unique :id
46
+ end
47
+ end
48
+ end
49
+ end
50
+
@@ -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,33 @@
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
+ attributes.merge!(record).merge!(:new_record => false)
16
+ klass.new_without_change_tracking attributes
17
+ end
18
+
19
+ def to_record(document)
20
+ { :id => document.id,
21
+ :created_at => document.created_at,
22
+ :updated_at => time.new,
23
+ :attributes => serialize(document) }
24
+ end
25
+
26
+ protected
27
+ def serialize(document)
28
+ attrs = document.to_hash.reject { |k,v| RESERVED_ATTRS.include?(k) }
29
+ serializer.generate(attrs)
30
+ end
31
+ end
32
+ end
33
+