ohm-composite 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZGQwNWM5MWVhM2ZlZTA1MzQ2ZjFhZjQ3YzdhOWJjZDliNjc0MzZmNg==
5
+ data.tar.gz: !binary |-
6
+ NTI0YzA2ZjBmZDRiYjU2NGNjYzAxYTA5NGViNDI4MWM3MGNhYjNlNg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MDdjNGI1NTdjYjk1NWQ4NGRlOThmZDI3OTQxZThlY2ExMGNkZjlmZTI1NmJk
10
+ ZDg4ZjI0MWFjNDZhNjk0NTgwZGYyM2RkOGRjODNiMmM1OTNlM2FkMDZiOGM5
11
+ ZDFiMzU1OGY3ZWU3ZDFmNDkxNzZiNmNkMDEwNDRlNTFmZjg2ZGI=
12
+ data.tar.gz: !binary |-
13
+ NDc1NGQ1YTlkZjg4ZGZhNTZiOTcxYTYzMzAyZTM0Nzc5ZTJlNmY2ZjU0MDYw
14
+ NjcyMjE0N2IxNWE3NjExY2Y2MjZhODRhZjY3MWI3NTc4YmE3N2E1ZDRhNmY1
15
+ ODAzYzExNjE4ODQwZDY4NjAzZWY4Yzg4M2FkNGZmN2YwOTI1MjM=
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ Ohm::Composite
2
+ ==============
3
+
4
+ Provides composite regular and unique indices for [Ohm][1].
5
+
6
+ Usage
7
+ -----
8
+
9
+ ```ruby
10
+ require "ohm/composite"
11
+
12
+ class Post
13
+ include Ohm::Composite
14
+
15
+ attribute :user_id
16
+ attribute :date
17
+ attribute :slug
18
+
19
+ composite_index [:user_id, :date]
20
+ composite_unique [:user_id, :slug]
21
+ end
22
+
23
+ Post.composite_find(user_id: 1, date: Date.today)
24
+ Post.composite_with(user_id: 1, slug: "lorem-ipsum")
25
+ ```
26
+
27
+ "Find or create"
28
+ ----------------
29
+
30
+ Often times you need to find an instance by its uniqueness, or create
31
+ it if it's not found.
32
+
33
+ For this purpose there's `composite_with_or_create`:
34
+
35
+ ```ruby
36
+ Post.composite_with_or_create(user_id: 1, slug: "lorem-ipsum")
37
+ ```
38
+
39
+ This is guaranteed to be contention-safe: if two competing threads or
40
+ processes end up in the create branch, only one will win and the other
41
+ will receive the created instance. None of them will raise exceptions
42
+ nor receive nils.
43
+
44
+ License
45
+ -------
46
+
47
+ See `UNLICENSE`. With love, from [Educabilia](http://educabilia.com).
48
+
49
+ [1]: https://github.com/soveran/ohm
data/UNLICENSE ADDED
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <http://unlicense.org/>
@@ -0,0 +1,66 @@
1
+ require "ohm"
2
+
3
+ module Ohm::Composite
4
+ VERSION = "0.1.0"
5
+
6
+ def self.included(model)
7
+ model.extend(Macros)
8
+ end
9
+
10
+ def self.method_name(attrs)
11
+ :"_composite_#{join(attrs, "_")}"
12
+ end
13
+
14
+ def self.values(values)
15
+ join(values, ":")
16
+ end
17
+
18
+ def self.join(elements, char)
19
+ replacement = char * 2
20
+
21
+ elements.map { |e| e.to_s.gsub(char, replacement) }.join(char)
22
+ end
23
+
24
+ module Macros
25
+ def composite_unique(attrs)
26
+ unique(_composite(attrs))
27
+ end
28
+
29
+ def composite_index(attrs)
30
+ index(_composite(attrs))
31
+ end
32
+
33
+ def composite_with(hash)
34
+ keys = hash.keys.sort
35
+ values = keys.map { |k| hash[k] }
36
+
37
+ with(Ohm::Composite.method_name(keys), Ohm::Composite.values(values))
38
+ end
39
+
40
+ def composite_find(hash)
41
+ keys = hash.keys.sort
42
+ values = keys.map { |k| hash[k] }
43
+
44
+ find(Ohm::Composite.method_name(keys) => Ohm::Composite.values(values))
45
+ end
46
+
47
+ def composite_with_or_create(hash)
48
+ composite_with(hash) ||
49
+ begin
50
+ create(hash)
51
+ rescue Ohm::UniqueIndexViolation
52
+ composite_with(hash)
53
+ end
54
+ end
55
+
56
+ def _composite(attrs)
57
+ attrs = attrs.sort
58
+
59
+ method_name = Ohm::Composite.method_name(attrs)
60
+
61
+ define_method(method_name) { Ohm::Composite.values(attrs.map { |att| send(att) }) }
62
+
63
+ method_name
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,16 @@
1
+ require File.expand_path("lib/ohm/composite", File.dirname(__FILE__))
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "ohm-composite"
5
+ s.version = Ohm::Composite::VERSION
6
+ s.summary = "Composite (regular and unique) indices for Ohm."
7
+ s.authors = ["Educabilia", "Damian Janowski"]
8
+ s.email = ["opensource@educabilia.com", "djanowski@dimaion.com"]
9
+ s.homepage = "https://github.com/educabilia/ohm-composite"
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
13
+
14
+ s.add_development_dependency "ohm"
15
+ s.add_development_dependency "cutest"
16
+ end
data/rakefile ADDED
@@ -0,0 +1,7 @@
1
+ task :test do
2
+ require "cutest"
3
+
4
+ Cutest.run(Dir["test/**/*_test.rb"])
5
+ end
6
+
7
+ task :default => :test
@@ -0,0 +1,29 @@
1
+ require_relative "prelude"
2
+
3
+ class Post < Ohm::Model
4
+ include Ohm::Composite
5
+
6
+ attribute :foo
7
+ attribute :bar
8
+ attribute :baz
9
+
10
+ attribute :bar_baz
11
+ attribute :baz_foo
12
+
13
+ composite_unique [:foo, :bar_baz]
14
+ composite_unique [:bar, :baz_foo]
15
+ end
16
+
17
+ scope do
18
+ test "does not confuse names" do
19
+ post1 = Post.create(foo: "foo", bar_baz: "bar_baz")
20
+
21
+ assert_equal post1, Post.composite_with(foo: "foo", bar_baz: "bar_baz")
22
+ assert_equal nil, Post.composite_with(bar: "bar_baz", baz_foo: "foo")
23
+
24
+ post2 = Post.create(bar: "bar_baz", baz_foo: "foo")
25
+
26
+ assert_equal post1, Post.composite_with(foo: "foo", bar_baz: "bar_baz")
27
+ assert_equal post2, Post.composite_with(bar: "bar_baz", baz_foo: "foo")
28
+ end
29
+ end
@@ -0,0 +1,99 @@
1
+ require_relative "prelude"
2
+
3
+ class Post < Ohm::Model
4
+ include Ohm::Composite
5
+
6
+ attribute :date
7
+ attribute :slug
8
+
9
+ composite_index [:date, :slug]
10
+ end
11
+
12
+ class Rating < Ohm::Model
13
+ include Ohm::Composite
14
+
15
+ attribute :user_id
16
+ attribute :post_id
17
+
18
+ composite_unique [:user_id, :post_id]
19
+ end
20
+
21
+ scope do
22
+ def assert_empty(value)
23
+ flunk("#{value} is not empty.") unless value.empty?
24
+ success
25
+ end
26
+
27
+ test "find" do
28
+ post = Post.create(date: "2013-03-23", slug: "lorem-ipsum")
29
+
30
+ assert_equal [post], Post.composite_find(date: "2013-03-23", slug: "lorem-ipsum").to_a
31
+ assert_empty Post.composite_find(date: "2013-03-23", slug: "dolor-sit").to_a
32
+ end
33
+
34
+ test "find with different hash order" do
35
+ post = Post.create(date: "2013-03-23", slug: "lorem-ipsum")
36
+
37
+ assert_equal [post], Post.composite_find(slug: "lorem-ipsum", date: "2013-03-23").to_a
38
+ end
39
+
40
+ test "fails with unknown indices" do
41
+ assert_raise { Post.composite_find(slug: "foo") }
42
+ end
43
+
44
+ test "does not confuse values when joining" do
45
+ post1 = Post.create(date: "2013-03-23:lorem", slug: "ipsum")
46
+ post2 = Post.create(date: "2013-03-23", slug: "lorem:ipsum")
47
+
48
+ assert_equal [post1], Post.composite_find(date: "2013-03-23:lorem", slug: "ipsum").to_a
49
+ assert_equal [post2], Post.composite_find(date: "2013-03-23", slug: "lorem:ipsum").to_a
50
+ end
51
+ end
52
+
53
+ scope do
54
+ test "unique" do
55
+ Rating.create(user_id: 1, post_id: 2)
56
+
57
+ assert_raise(Ohm::UniqueIndexViolation) do
58
+ Rating.create(user_id: 1, post_id: 2)
59
+ end
60
+
61
+ assert_raise(Ohm::UniqueIndexViolation) do
62
+ Rating.create(post_id: 2, user_id: 1)
63
+ end
64
+ end
65
+
66
+ test "find unique" do
67
+ rating = Rating.create(user_id: 1, post_id: 2)
68
+
69
+ assert_equal rating, Rating.composite_with(user_id: 1, post_id: 2)
70
+ assert_equal rating, Rating.composite_with(post_id: 2, user_id: 1)
71
+ end
72
+ end
73
+
74
+ scope do
75
+ test "with or create" do
76
+ rating = Rating.composite_with_or_create(user_id: 1, post_id: 2)
77
+
78
+ assert rating
79
+
80
+ assert_equal rating, Rating.composite_with_or_create(user_id: 1, post_id: 2)
81
+ end
82
+
83
+ test "contention" do
84
+ results = []
85
+
86
+ threads = Array.new(10) do
87
+ Thread.new do
88
+ 10.times do |i|
89
+ results << Rating.composite_with_or_create(user_id: i, post_id: 2)
90
+ end
91
+ end
92
+ end
93
+
94
+ threads.each(&:join)
95
+
96
+ assert !results.include?(nil)
97
+ assert_equal 10, results.uniq.size
98
+ end
99
+ end
data/test/prelude.rb ADDED
@@ -0,0 +1,11 @@
1
+ ENV["REDIS_URL"] = "redis://localhost:6379/15"
2
+
3
+ require "cutest"
4
+
5
+ $VERBOSE = 1
6
+
7
+ require_relative "../lib/ohm/composite"
8
+
9
+ setup do
10
+ Ohm.flush
11
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ohm-composite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Educabilia
8
+ - Damian Janowski
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ohm
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ! '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: cutest
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description:
43
+ email:
44
+ - opensource@educabilia.com
45
+ - djanowski@dimaion.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - README.md
51
+ - UNLICENSE
52
+ - lib/ohm/composite.rb
53
+ - ohm-composite.gemspec
54
+ - rakefile
55
+ - test/collisions_test.rb
56
+ - test/composite_test.rb
57
+ - test/prelude.rb
58
+ homepage: https://github.com/educabilia/ohm-composite
59
+ licenses: []
60
+ metadata: {}
61
+ post_install_message:
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubyforge_project:
77
+ rubygems_version: 2.0.0
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: Composite (regular and unique) indices for Ohm.
81
+ test_files: []