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,41 @@
1
+ require 'friendly/document/mixin'
2
+
3
+ module Friendly
4
+ module Document
5
+ module Convenience
6
+ extend Mixin
7
+
8
+ module ClassMethods
9
+ attr_writer :collection_klass
10
+
11
+ def collection_klass
12
+ @collection_klass ||= WillPaginate::Collection
13
+ end
14
+
15
+ def find(id)
16
+ doc = first(:id => id)
17
+ raise RecordNotFound, "Couldn't find #{name}/#{id}" if doc.nil?
18
+ doc
19
+ end
20
+
21
+ def paginate(conditions)
22
+ query = query(conditions)
23
+ count = count(query)
24
+ collection = collection_klass.new(query.page, query.per_page, count)
25
+ collection.replace(all(query))
26
+ end
27
+
28
+ def create(attributes = {})
29
+ doc = new(attributes)
30
+ doc.save
31
+ doc
32
+ end
33
+ end
34
+
35
+ def update_attributes(attributes)
36
+ self.attributes = attributes
37
+ save
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ module Friendly
2
+ module Document
3
+ module Mixin
4
+ # FIXME: I'm not in love with this. But, I also don't think it's the
5
+ # end of the world.
6
+ def included(klass)
7
+ if klass.const_defined?(:ClassMethods)
8
+ klass.const_get(:ClassMethods).send(:include, const_get(:ClassMethods))
9
+ else
10
+ klass.send(:extend, const_get(:ClassMethods))
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ require 'friendly/document/mixin'
2
+
3
+ module Friendly
4
+ module Document
5
+ module Scoping
6
+ extend Mixin
7
+
8
+ module ClassMethods
9
+ attr_writer :scope_proxy
10
+
11
+ def scope_proxy
12
+ @scope_proxy ||= ScopeProxy.new(self)
13
+ end
14
+
15
+ # Add a named scope to this Document.
16
+ #
17
+ # e.g.
18
+ #
19
+ # class Post
20
+ # indexes :created_at
21
+ # named_scope :recent, :order! => :created_at.desc
22
+ # end
23
+ #
24
+ # Then, you can access the recent posts with:
25
+ #
26
+ # Post.recent.all
27
+ # ...or...
28
+ # Post.recent.first
29
+ #
30
+ # Both #all and #first also take additional parameters:
31
+ #
32
+ # Post.recent.all(:author_id => @author.id)
33
+ #
34
+ # Scopes are also chainable. See the README or Friendly::Scope docs for details.
35
+ #
36
+ # @param [Symbol] name the name of the scope.
37
+ # @param [Hash] parameters the query that this named scope will perform.
38
+ #
39
+ def named_scope(name, parameters)
40
+ scope_proxy.add_named(name, parameters)
41
+ end
42
+
43
+ # Returns boolean based on whether the Document has a scope by a particular name.
44
+ #
45
+ # @param [Symbol] name The name of the scope in question.
46
+ #
47
+ def has_named_scope?(name)
48
+ scope_proxy.has_named_scope?(name)
49
+ end
50
+
51
+ # Create an ad hoc scope on this Document.
52
+ #
53
+ # e.g.
54
+ #
55
+ # scope = Post.scope(:order! => :created_at)
56
+ # scope.all # => [#<Post>, #<Post>]
57
+ #
58
+ # @param [Hash] parameters the query parameters to create the scope with.
59
+ #
60
+ def scope(parameters)
61
+ scope_proxy.ad_hoc(parameters)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,63 @@
1
+ require 'friendly/document/mixin'
2
+
3
+ module Friendly
4
+ module Document
5
+ module Storage
6
+ extend Mixin
7
+
8
+ module ClassMethods
9
+ attr_writer :storage_proxy, :query_klass
10
+
11
+ def create_tables!
12
+ storage_proxy.create_tables!
13
+ end
14
+
15
+ def storage_proxy
16
+ @storage_proxy ||= StorageProxy.new(self)
17
+ end
18
+
19
+ def indexes(*args)
20
+ storage_proxy.add(args)
21
+ end
22
+
23
+ def caches_by(*fields)
24
+ options = fields.last.is_a?(Hash) ? fields.pop : {}
25
+ storage_proxy.cache(fields, options)
26
+ end
27
+
28
+ def first(query)
29
+ storage_proxy.first(query(query))
30
+ end
31
+
32
+ def all(query)
33
+ storage_proxy.all(query(query))
34
+ end
35
+
36
+ def count(conditions)
37
+ storage_proxy.count(query(conditions))
38
+ end
39
+
40
+ def query_klass
41
+ @query_klass ||= Query
42
+ end
43
+
44
+ protected
45
+ def query(conditions)
46
+ conditions.is_a?(Query) ? conditions : query_klass.new(conditions)
47
+ end
48
+ end
49
+
50
+ def save
51
+ new_record? ? storage_proxy.create(self) : storage_proxy.update(self)
52
+ end
53
+
54
+ def destroy
55
+ storage_proxy.destroy(self)
56
+ end
57
+
58
+ def storage_proxy
59
+ self.class.storage_proxy
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,56 @@
1
+ require 'friendly/table'
2
+
3
+ module Friendly
4
+ class DocumentTable < Table
5
+ attr_reader :klass, :translator
6
+
7
+ def initialize(klass, datastore=Friendly.datastore, translator=Translator.new)
8
+ super(datastore)
9
+ @klass = klass
10
+ @translator = translator
11
+ end
12
+
13
+ def table_name
14
+ klass.table_name
15
+ end
16
+
17
+ def satisfies?(query)
18
+ query.conditions.keys == [:id] && !query.order
19
+ end
20
+
21
+ def create(document)
22
+ record = translator.to_record(document)
23
+ datastore.insert(document, record)
24
+ update_document(document, record)
25
+ end
26
+
27
+ def update(document)
28
+ record = translator.to_record(document)
29
+ datastore.update(document, document.id, record)
30
+ update_document(document, record)
31
+ end
32
+
33
+ def destroy(document)
34
+ datastore.delete(document, document.id)
35
+ end
36
+
37
+ def first(query)
38
+ record = datastore.first(klass, query)
39
+ record && to_object(record)
40
+ end
41
+
42
+ def all(query)
43
+ datastore.all(klass, query).map { |r| to_object(r) }
44
+ end
45
+
46
+ protected
47
+ def update_document(document, record)
48
+ attrs = record.reject { |k,v| k == :attributes }.merge(:new_record => false)
49
+ document.attributes = attrs
50
+ end
51
+
52
+ def to_object(record)
53
+ translator.to_object(klass, record)
54
+ end
55
+ end
56
+ end
@@ -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,50 @@
1
+ module Friendly
2
+ class Indexer
3
+ class << self
4
+ attr_accessor :objects_per_iteration
5
+
6
+ def populate(klass, *fields)
7
+ instance.populate(klass, klass.storage_proxy.index_for_fields(fields))
8
+ end
9
+
10
+ def instance
11
+ @instance ||= new
12
+ end
13
+ end
14
+
15
+ self.objects_per_iteration = 100
16
+
17
+ attr_reader :datastore, :translator
18
+
19
+ def initialize(datastore = Friendly.datastore, translator = Translator.new)
20
+ @datastore = datastore
21
+ @translator = translator
22
+ end
23
+
24
+ def populate(klass, index)
25
+ count = 0
26
+ loop do
27
+ rows = datastore.all(klass, Query.new(:offset! => count,
28
+ :limit! => objects_per_iteration,
29
+ :order! => :added_id.asc))
30
+ rows.each do |attrs|
31
+ begin
32
+ index.create(translator.to_object(klass, attrs))
33
+ rescue Sequel::DatabaseError
34
+ # we can safely swallow this exception because if we've gotten
35
+ # to this point, we can be pretty sure that it's a duplicate
36
+ # key error, which just means that the object already exists
37
+ # in the index
38
+ end
39
+ end
40
+ break if rows.length < objects_per_iteration
41
+ count += objects_per_iteration
42
+ end
43
+ end
44
+
45
+ protected
46
+ def objects_per_iteration
47
+ self.class.objects_per_iteration
48
+ end
49
+ end
50
+ 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