ohm-composite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []