embeds_many 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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: df5b6c590bea371b92b51fcc0d997179a9a31f79
4
+ data.tar.gz: 822d11065876bd3721db8ff8d5ea42161eab83da
5
+ SHA512:
6
+ metadata.gz: edb630f9cec39da2062f0fe599074357d0427a071f8e489cf0a07ecadad47d52cf84f4f35d606f5a365a62f5f27efbb502dc833d1e3fee017169b76c8204529d
7
+ data.tar.gz: f026f9f1796fa051ddd02f2d42afec6f8b37a9e47568b7de09fab2910b0b51f230e744c92d78602b3701fd09c9ee0f3faa99ccc2ae0bf995c18be9167c15aa02
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile ~/.gitignore_global
6
+
7
+ # Ignore bundler config
8
+ /.bundle
9
+ /doc
10
+ /.yardoc
11
+ /Gemfile.lock
12
+ spec/*.log
13
+ *.swp
14
+ *.bak
15
+ *.gem
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.1
6
+
7
+ script: "bundle exec rake spec"
8
+
9
+ before_script:
10
+ - psql -c 'create database embeds_many_test;' -U postgres
11
+ - psql embeds_many_test -c 'create extension hstore;' -U postgres
12
+
13
+ addons:
14
+ postgresql: '9.3'
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in embeds_many.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2014 Notion Labs
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,106 @@
1
+ EmbedsMany
2
+ ==============
3
+
4
+ [![Build Status](https://travis-ci.org/NotionLabs/embeds_many.svg)](https://travis-ci.org/NotionLabs/embeds_many)
5
+
6
+ EmbedsMany allows programmers to work with embedded records the same way as activerecord objects, with the power of PostgreSQL's hstore and array.
7
+
8
+ **NOTE**: EmbedsMany only works with Rails/ActiveRecord `4.0.4` or above. To use EmbedsMany, you must use PostgreSQL.
9
+
10
+ ## Usage
11
+
12
+ ### Installation
13
+
14
+ To use the gem, add following to your Gemfile:
15
+
16
+ ``` ruby
17
+ gem 'embeds_many'
18
+ ```
19
+
20
+ ### Setup Database
21
+
22
+ You need a `hstor array` column to hold the embedded records.
23
+
24
+ When creating a new table, use following:
25
+
26
+ ```ruby
27
+ create_table "users", force: true do |t|
28
+ t.string "name"
29
+ t.datetime "created_at"
30
+ t.datetime "updated_at"
31
+ t.hstore "tags", default: [], array: true
32
+ end
33
+ ```
34
+
35
+ When adding a column to existing table, use following:
36
+
37
+ ```ruby
38
+ add_column :users, :tags, :hstore, array: true, default: []
39
+ ```
40
+
41
+ ### Setup Model
42
+
43
+ ```ruby
44
+ class User < ActiveRecord::Base
45
+ # many embedded tags
46
+ embeds_many :tags
47
+
48
+ # validation tags
49
+ embedded :tags do
50
+ embedded_fields :name, :color
51
+
52
+ validates :name, uniqueness: true, presence: true
53
+ validates :color, presence: true
54
+
55
+ def as_json
56
+ {
57
+ id: id,
58
+ name: name,
59
+ color: color
60
+ }
61
+ end
62
+ end
63
+ end
64
+ ```
65
+
66
+ ### Work with embedded tags
67
+
68
+ ```ruby
69
+ # create new tag
70
+ @tag = user.tags.new(tag_params)
71
+ unless @tag.save
72
+ render json: { success: false, message: @tag.errors.full_messages.join('. ') }
73
+ end
74
+
75
+ # update
76
+ @tag = user.tags.find(params['id'])
77
+
78
+ if @tag.update(tag_params)
79
+ # do something
80
+ end
81
+
82
+ # destroy
83
+ @tag = user.tags.find(params['id'])
84
+
85
+ @tag.destroy
86
+ ```
87
+
88
+ ## Development
89
+
90
+ All pull requests are welcome.
91
+
92
+ ### Setup development environment
93
+
94
+ 1. Run `bundle install`
95
+ 2. Run `rake db:create` to create database
96
+ 3. Run `rake spec` to run specs
97
+
98
+ ### Development Guide
99
+
100
+ - Follow established and good convention
101
+ - Write code and specs
102
+ - Send pull request
103
+
104
+ ## License
105
+
106
+ Copyright (c) 2014 Notion Labs, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => :spec
6
+
7
+ namespace :db do
8
+ task :create_user do
9
+ `createuser -s -r postgres`
10
+ end
11
+
12
+ task :create do
13
+ `psql -c 'create database embeds_many_test;' -U postgres`
14
+ `psql embeds_many_test -c 'create extension hstore;' -U postgres`
15
+ end
16
+
17
+ task :drop do
18
+ `dropdb 'embeds_many_test'`
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "embeds_many/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "embeds_many"
7
+ s.version = EmbedsMany::VERSION
8
+ s.authors = ["liufengyun"]
9
+ s.email = ["liufengyunchina@gmail.com"]
10
+ s.homepage = "https://github.com/notionlabs/embeds_many"
11
+ s.summary = %q{Embedded records based on the power of PostgreSQL's hstore and array}
12
+ s.description = %q{EmbedsMany allows programmers to work with embedded records the same way as activerecord objects}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_dependency('activerecord','>= 4.0.4')
20
+
21
+ s.add_development_dependency('rspec')
22
+ s.add_development_dependency('database_cleaner')
23
+ s.add_development_dependency('pg')
24
+ s.add_development_dependency('rake')
25
+ end
@@ -0,0 +1,9 @@
1
+ require "active_record"
2
+ require "active_model"
3
+
4
+ require "embeds_many/version"
5
+ require "embeds_many/base"
6
+ require "embeds_many/child"
7
+ require "embeds_many/child_collection"
8
+
9
+ ActiveRecord::Base.extend(EmbedsMany::Base)
@@ -0,0 +1,20 @@
1
+ module EmbedsMany
2
+ module Base
3
+ def embeds_many(field)
4
+ child_klass = instance_variable_set "@#{field}_klass", EmbedsMany::Child.clone
5
+ child_klass.instance_variable_set "@field_name", field
6
+
7
+ # rewrite association
8
+ define_method field do
9
+ instance_variable_get("@#{field}_collection") ||
10
+ instance_variable_set("@#{field}_collection", ChildrenCollection.new(self, field, child_klass))
11
+ end
12
+ end
13
+
14
+ # define validations and helper instance methods
15
+ def embedded(field, &block)
16
+ child_klass = instance_variable_get "@#{field}_klass"
17
+ child_klass.class_eval(&block)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,131 @@
1
+ module EmbedsMany
2
+ class Child
3
+ extend ActiveModel::Validations::ClassMethods
4
+ include ActiveModel::Dirty
5
+ include ActiveModel::Validations
6
+
7
+ # add accessors for fields
8
+ def self.embedded_fields(*fields)
9
+ fields.each do |field_name|
10
+ define_method(field_name) do
11
+ @attributes[field_name]
12
+ end
13
+
14
+ define_method("#{field_name}=") do |val|
15
+ @attributes[field_name] = val
16
+ end
17
+ end
18
+ end
19
+
20
+ attr_accessor :parent
21
+ embedded_fields :id
22
+
23
+ class UniquenessValidator < ActiveModel::EachValidator
24
+ def validate_each(record, attribute, value)
25
+ if record.exists_in_parent? {|item| item.key?(attribute.to_s) && item[attribute.to_s] == value && record.id.to_s != item['id'] }
26
+ record.errors.add attribute, "#{value} is already taken"
27
+ end
28
+ end
29
+ end
30
+
31
+ # validation requires model name
32
+ def self.model_name
33
+ ActiveModel::Name.new(self, nil, @field_name.to_s.classify)
34
+ end
35
+
36
+ def initialize(attrs)
37
+ @attributes = ActiveSupport::HashWithIndifferentAccess.new
38
+
39
+ attrs.each do |name, value|
40
+ if respond_to? "#{name}="
41
+ send("#{name}=", value)
42
+ else
43
+ @attributes[name] = value
44
+ end
45
+ end
46
+ end
47
+
48
+ def save
49
+ return false unless self.valid?
50
+
51
+ if new_record?
52
+ save_new_record!
53
+ else
54
+ save_existing_record!
55
+ end
56
+ end
57
+
58
+ def destroy
59
+ # tell rails the field will change
60
+ parent.send "#{field_name}_will_change!"
61
+
62
+ parent.read_attribute(field_name).delete_if {|t| t['id'] == self.id}
63
+
64
+ if parent.save
65
+ true
66
+ else
67
+ parent.send "#{field_name}=", parent.send("#{field_name}_was")
68
+ false
69
+ end
70
+ end
71
+
72
+ def update(attrs)
73
+ @attributes.update(attrs)
74
+
75
+ save
76
+ end
77
+
78
+ def new_record?
79
+ self.id.nil?
80
+ end
81
+
82
+ def exists_in_parent?(&block)
83
+ parent.read_attribute(field_name).any? &block
84
+ end
85
+
86
+ private
87
+
88
+ def save_new_record!
89
+ @attributes[:id] = generate_id!
90
+
91
+ # tell rails the field will change
92
+ parent.send "#{field_name}_will_change!"
93
+
94
+ parent.read_attribute(field_name) << @attributes.to_hash
95
+
96
+ if parent.save
97
+ true
98
+ else
99
+ @attributes.id = nil
100
+ # restore old value
101
+ parent.send "#{field_name}=", parent.send("#{field_name}_was")
102
+ false
103
+ end
104
+ end
105
+
106
+ def save_existing_record!
107
+ # tell rails the field will change
108
+ parent.send "#{field_name}_will_change!"
109
+
110
+ record = parent.read_attribute(field_name).detect {|t| t['id'].to_i == self.id.to_i }
111
+ record.merge!(@attributes)
112
+
113
+ if parent.save
114
+ true
115
+ else
116
+ # restore old value
117
+ parent.send "#{field_name}=", parent.send("#{field_name}_was")
118
+ false
119
+ end
120
+ end
121
+
122
+ def generate_id!
123
+ max_id = parent.read_attribute(field_name).inject(0) {|max, t| if t['id'].to_i > max then t['id'].to_i else max end }
124
+ max_id + 1
125
+ end
126
+
127
+ def field_name
128
+ self.class.instance_variable_get("@field_name")
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,39 @@
1
+ module EmbedsMany
2
+ class ChildrenCollection
3
+ def new(attrs)
4
+ @child_klass.new(attrs.merge(parent: @obj))
5
+ end
6
+
7
+ def create(attrs)
8
+ record = @child_klass.new(attrs.merge(parent: @obj))
9
+
10
+ record.save
11
+
12
+ record
13
+ end
14
+
15
+ def initialize(obj, field, child_klass)
16
+ @obj = obj
17
+ @field = field
18
+ @child_klass = child_klass
19
+ end
20
+
21
+ def find(id)
22
+ attrs = @obj.read_attribute(@field).find {|child| child['id'].to_i == id.to_i }
23
+
24
+ attrs && @child_klass.new(attrs.merge(parent: @obj))
25
+ end
26
+
27
+ # all records
28
+ def all
29
+ @obj.read_attribute(@field).map do |attrs|
30
+ @child_klass.new(attrs.merge(parent: @obj))
31
+ end
32
+ end
33
+
34
+ # pass unhandled message to children array
35
+ def method_missing(symbol, *args, &block)
36
+ all.send(symbol, *args, &block)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,3 @@
1
+ module EmbedsMany
2
+ VERSION = "0.1.0"
3
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,4 @@
1
+ test:
2
+ adapter: postgresql
3
+ database: embeds_many_test
4
+ username: postgres
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ describe User do
4
+ let(:user) { User.create(name: 'test') }
5
+
6
+ it "should has no tags" do
7
+ user.read_attribute(:tags).should eq([])
8
+ end
9
+
10
+ describe "tags" do
11
+ describe "create" do
12
+ it "should be able to create one tag" do
13
+ tag = user.tags.new(name: 'bug', color: 'red')
14
+
15
+ tag.save.should be_true
16
+ tag.id.should_not be_nil
17
+
18
+ user.reload.tags.any? {|t| t.name == 'bug'}.should be_true
19
+ end
20
+
21
+ it "should be unable to create tags with duplicate name" do
22
+ user.tags.new(name: 'bug', color: 'red').save.should be_true
23
+
24
+ tag = user.tags.new(name: 'bug', color: 'green')
25
+
26
+ tag.save.should be_false
27
+ tag.errors[:name].should_not be_empty
28
+ end
29
+
30
+ it "should be unable to create tags without color" do
31
+ tag = user.tags.new(name: 'bug')
32
+
33
+ tag.save.should be_false
34
+ tag.errors[:color].should_not be_empty
35
+ end
36
+ end
37
+
38
+ it "should be able to update record" do
39
+ tag = user.tags.create(name: 'bug', color: 'red')
40
+
41
+ user.reload.tags.any? {|t| t.name == 'bug'}.should be_true
42
+
43
+ tag.update(color: 'yellow').should be_true
44
+
45
+ user.reload.tags.any? {|t| t.color == 'yellow'}.should be_true
46
+ end
47
+
48
+ it "should be able to destroy" do
49
+ tag = user.tags.create(name: 'bug', color: 'red')
50
+
51
+ tag.destroy.should be_true
52
+
53
+ user.reload.tags.any? {|t| t.name == 'bug'}.should be_false
54
+ end
55
+ end
56
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,30 @@
1
+ # Setup the db
2
+ ActiveRecord::Schema.define(:version => 1) do
3
+ create_table "users", force: true do |t|
4
+ t.string "name"
5
+ t.datetime "created_at"
6
+ t.datetime "updated_at"
7
+ t.hstore "tags", default: [], array: true
8
+ end
9
+ end
10
+
11
+ class User < ActiveRecord::Base
12
+ # many embedded tags
13
+ embeds_many :tags
14
+
15
+ # validation tags
16
+ embedded :tags do
17
+ embedded_fields :name, :color
18
+
19
+ validates :name, uniqueness: true, presence: true
20
+ validates :color, presence: true
21
+
22
+ def as_json
23
+ {
24
+ id: id,
25
+ name: name,
26
+ color: color
27
+ }
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'database_cleaner'
5
+ require 'embeds_many'
6
+
7
+ # connect to db
8
+ config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
9
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
10
+ ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'test'])
11
+
12
+ # setup database
13
+ require 'schema'
14
+
15
+ RSpec.configure do |config|
16
+ config.before(:suite) do
17
+ DatabaseCleaner.strategy = :transaction
18
+ DatabaseCleaner.clean_with(:truncation)
19
+ end
20
+
21
+ config.before(:each) do
22
+ DatabaseCleaner.start
23
+ end
24
+
25
+ config.after(:each) do
26
+ DatabaseCleaner.clean
27
+ end
28
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: embeds_many
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - liufengyun
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-03-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: database_cleaner
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pg
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: EmbedsMany allows programmers to work with embedded records the same
84
+ way as activerecord objects
85
+ email:
86
+ - liufengyunchina@gmail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - .gitignore
92
+ - .rspec
93
+ - .travis.yml
94
+ - Gemfile
95
+ - LICENSE
96
+ - README.markdown
97
+ - Rakefile
98
+ - embeds_many.gemspec
99
+ - lib/embeds_many.rb
100
+ - lib/embeds_many/base.rb
101
+ - lib/embeds_many/child.rb
102
+ - lib/embeds_many/child_collection.rb
103
+ - lib/embeds_many/version.rb
104
+ - spec/database.yml
105
+ - spec/embeds_many/user_spec.rb
106
+ - spec/schema.rb
107
+ - spec/spec_helper.rb
108
+ homepage: https://github.com/notionlabs/embeds_many
109
+ licenses: []
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.0.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Embedded records based on the power of PostgreSQL's hstore and array
131
+ test_files:
132
+ - spec/database.yml
133
+ - spec/embeds_many/user_spec.rb
134
+ - spec/schema.rb
135
+ - spec/spec_helper.rb