findable 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed73de3540dfd5ad802f506f399aa52cfa748a7f
4
- data.tar.gz: f35dc8a71965b62c822e6aa7a9070f733b043db5
3
+ metadata.gz: 79d36740a44c4a4a702069dab24e53b533e8db00
4
+ data.tar.gz: 2908163cf00a3546837f8a0fa5b3e20bad62b044
5
5
  SHA512:
6
- metadata.gz: 8ffdcafaef3953de07e19f63d804bd25e0c69db535d964b96c4f0da81d3f4cddcdcb1bd1ddebaccc2dee5e48fb04a1a4d234526830050dfb423d5ef903b7b68e
7
- data.tar.gz: 41d2b9c45cbd8b5902c66ca28779aa8d3bac5d453ce7912bb2578cd8180326598fb89ef77bf9f6a507233554cd74e4e8ece7a1ecd14e03527aab83720722a948
6
+ metadata.gz: 2761e6fbd194a70df6ecab268d507a4b689520b537c3210e83f9d1a59d1528a6df5085fcd23d9c5b129f60a3d8037135fa310e2a7c861f9a70be1a0a3fccd709
7
+ data.tar.gz: fdef311c4c618ee709ca488157753480ba2b2fc2bf61d6e1060d904d8aa9c9a1253c18d14d3e20fa3705a14581af019bd19e14950c2d024e73ec554b97d0c90b
@@ -1,6 +1,6 @@
1
1
  module Findable
2
2
  module Associations
3
- class Utils
3
+ module Utils
4
4
  def self.model_for(name, options = {})
5
5
  unless model_name = options[:class_name].presence
6
6
  name = options[:collection] ? name.to_s.singularize : name.to_s
@@ -5,7 +5,6 @@ require "findable/inspection"
5
5
  module Findable
6
6
  class Base
7
7
  include ActiveModel::Model
8
- include ActiveModel::AttributeMethods
9
8
  include Associations
10
9
  include Schema
11
10
  include Inspection
@@ -39,8 +38,14 @@ module Findable
39
38
  def find_by(conditions)
40
39
  if conditions.is_a?(Hash)
41
40
  conditions.symbolize_keys!
42
- if id = conditions.delete(:id)
43
- records = find_by_ids(id)
41
+ if index = conditions.keys.detect {|key| key.in?(indexes) }
42
+ value = conditions.delete(index)
43
+ if index == :id
44
+ records = find_by_ids(value)
45
+ else
46
+ records = find_by_index(index, value)
47
+ end
48
+
44
49
  case
45
50
  when records.empty? then nil
46
51
  when conditions.empty? then records.first
@@ -63,8 +68,14 @@ module Findable
63
68
 
64
69
  def where(conditions)
65
70
  conditions.symbolize_keys!
66
- if id = conditions.delete(:id)
67
- records = find_by_ids(id)
71
+ if index = conditions.keys.detect {|key| key.in?(indexes) }
72
+ value = conditions.delete(index)
73
+ if index == :id
74
+ records = find_by_ids(value)
75
+ else
76
+ records = find_by_index(index, value)
77
+ end
78
+
68
79
  if conditions.empty?
69
80
  collection!(records)
70
81
  else
@@ -84,10 +95,17 @@ module Findable
84
95
  end
85
96
  alias_method :create!, :create
86
97
 
98
+ ## Extension
99
+
100
+ def ordered_find(*_ids)
101
+ _ids.flatten!
102
+ find(_ids).ordered_find(_ids)
103
+ end
104
+
87
105
  ## Query APIs
88
106
 
89
- delegate :find_by_ids, :insert, to: :query
90
- delegate :count, :ids, :delete_all, to: :query
107
+ delegate :find_by_ids, :find_by_index, :insert, to: :query
108
+ delegate :count, :ids, :delete, :delete_all, to: :query
91
109
  alias_method :destroy_all, :delete_all
92
110
 
93
111
  def exists?(obj)
@@ -98,12 +116,6 @@ module Findable
98
116
  end
99
117
  end
100
118
 
101
- def delete(obj)
102
- if _id = id_from(obj)
103
- query.delete(_id)
104
- end
105
- end
106
-
107
119
  def query
108
120
  @_query ||= Query.new(self)
109
121
  end
@@ -67,6 +67,11 @@ module Findable
67
67
  })
68
68
  end
69
69
 
70
+ def ordered_find(*_ids)
71
+ _ids.flatten!
72
+ records.index_by(&:id).values_at(*_ids)
73
+ end
74
+
70
75
  def pluck(*columns)
71
76
  columns.flatten!
72
77
  return records.map {|record| record.attributes.values } if columns.empty?
@@ -31,6 +31,12 @@ module Findable
31
31
  @serializer.deserialize(redis.hmget(data_key, *Array(ids)), model)
32
32
  end
33
33
 
34
+ def find_by_index(index, value)
35
+ if ids = ids_from_index([index, value].join(":"))
36
+ find_by_ids(ids)
37
+ end
38
+ end
39
+
34
40
  def exists?(id)
35
41
  redis.hexists(data_key, id)
36
42
  end
@@ -43,28 +49,51 @@ module Findable
43
49
  object.id,
44
50
  @serializer.serialize(object.attributes)
45
51
  )
52
+ update_index(object)
46
53
  end
47
54
  object
48
55
  end
49
56
 
50
57
  def import(hashes)
51
58
  lock do
52
- auto_incremented = hashes.each_with_object([]) do |hash, obj|
59
+ indexes = Hash.new {|h, k| h[k] = [] }
60
+ values = hashes.each_with_object([]) do |hash, obj|
61
+ hash = hash.with_indifferent_access
53
62
  hash["id"] = auto_incremented_id(hash["id"])
54
63
  obj << hash["id"]
55
64
  obj << @serializer.serialize(hash)
65
+
66
+ if model.index_defined?
67
+ model.indexes.each_with_object([]) do |name, obj|
68
+ next if name == :id
69
+ indexes[[name, hash[name]].join(":")] << hash["id"]
70
+ end
71
+ end
72
+ end
73
+ redis.hmset(data_key, *values)
74
+ if indexes.present?
75
+ attrs = indexes.map {|k, v| [k, @serializer.serialize(v)] }.flatten
76
+ redis.hmset(index_key, *attrs)
56
77
  end
57
- redis.hmset(data_key, *auto_incremented)
58
78
  end
59
79
  end
60
80
 
61
- def delete(id)
62
- redis.hdel(data_key, id)
81
+ def delete(object)
82
+ if model.index_defined?
83
+ model.indexes.each do |name|
84
+ next if name == :id
85
+ if value = object.public_send("#{name}_was") || object.public_send(name)
86
+ redis.hdel(index_key, value)
87
+ end
88
+ end
89
+ end
90
+
91
+ redis.hdel(data_key, object.id)
63
92
  end
64
93
 
65
94
  def delete_all
66
95
  redis.multi do
67
- [data_key, info_key].each {|key| redis.del(key) }
96
+ [data_key, info_key, index_key].each {|key| redis.del(key) }
68
97
  end
69
98
  end
70
99
 
@@ -73,6 +102,32 @@ module Findable
73
102
  Lock.new(lock_key, thread_key).lock { yield }
74
103
  end
75
104
 
105
+ def update_index(object)
106
+ if model.index_defined?
107
+ indexes = model.indexes.each_with_object([]) {|name, obj|
108
+ next if name == :id || object.public_send("#{name}_changed?")
109
+
110
+ if old_value = object.public_send("#{name}_was")
111
+ old_index_key = [name, old_value].join(":")
112
+
113
+ if (old_ids = ids_from_index(old_index_key)).present?
114
+ new_ids = old_ids.reject {|id| id == object.id }
115
+ if new_ids.empty?
116
+ redis.hdel(index_key, old_index_key)
117
+ else
118
+ obj << old_index_key
119
+ obj << @serializer.serialize(new_ids)
120
+ end
121
+ end
122
+ end
123
+
124
+ obj << [name, object.public_send(name)].join(":")
125
+ obj << object.id
126
+ }
127
+ redis.hmset(index_key, *indexes)
128
+ end
129
+ end
130
+
76
131
  private
77
132
  def auto_incremented_id(id)
78
133
  if id.present?
@@ -86,5 +141,11 @@ module Findable
86
141
  redis.hincrby(info_key, AUTO_INCREMENT_KEY, 1)
87
142
  end
88
143
  end
144
+
145
+ def ids_from_index(index_name)
146
+ if ids = redis.hget(index_key, index_name)
147
+ @serializer.deserialize(ids)
148
+ end
149
+ end
89
150
  end
90
151
  end
@@ -5,6 +5,9 @@ module Findable
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
+ include ActiveModel::AttributeMethods
9
+ include ActiveModel::Dirty
10
+
8
11
  attribute_method_suffix "="
9
12
  attribute_method_suffix "?"
10
13
  end
@@ -14,19 +17,29 @@ module Findable
14
17
  @_column_names ||= [:id]
15
18
  end
16
19
 
20
+ def indexes
21
+ @_indexes ||= [:id]
22
+ end
23
+
24
+ def index_defined?
25
+ indexes.size > 1
26
+ end
27
+
17
28
  def define_field(*args)
18
29
  options = args.extract_options!
19
30
  name = args.first
20
31
  if !public_method_defined?(name) || options.present?
21
32
  define_attribute_methods name
22
- conversion = Findable::Schema::Conversion.for(options[:type])
33
+ conversion = Findable::Schema::Conversion.to(options[:type])
23
34
  define_method(name) { conversion.call(attributes[name.to_sym]) }
35
+ indexes << name.to_sym if options[:index]
24
36
  column_names << name.to_sym
25
37
  end
26
38
  end
27
39
  end
28
40
 
29
41
  def attribute=(attr, value)
42
+ public_send("#{attr}_will_change!")
30
43
  attributes[attr] = value
31
44
  end
32
45
 
@@ -1,12 +1,12 @@
1
1
  module Findable
2
2
  module Schema
3
- class Conversion
3
+ module Conversion
4
4
  class << self
5
5
  FALSE_VALUE = ["false", "0"]
6
6
 
7
- def for(type)
7
+ def to(type)
8
8
  return types[:default] if type.nil?
9
- types[type] || add_type!(type)
9
+ types[type.to_sym] || add_type!(type)
10
10
  end
11
11
 
12
12
  def types
@@ -16,8 +16,8 @@ module Findable
16
16
  end
17
17
 
18
18
  def add_type!(type)
19
- return type if type.respond_to?(:call)
20
- types[type.to_sym] = method(type)
19
+ return type if type.is_a?(Proc)
20
+ types[type.to_sym] = method(type).to_proc
21
21
  end
22
22
 
23
23
  def clear_types
@@ -1,14 +1,50 @@
1
+ require "yaml"
2
+ require "csv"
3
+
1
4
  module Findable
2
- class Seed
5
+ class UnknownSeedDir < FindableError
6
+ def initialize(path)
7
+ if path
8
+ super("Couldn't find #{path}")
9
+ else
10
+ super("There is no configuration")
11
+ end
12
+ end
13
+ end
14
+
15
+ class Seed < Struct.new(:seed_files, :model_name)
3
16
  class << self
4
17
  def target_files(seed_dir: nil, seed_files: nil)
5
- target_dir = pathname(seed_dir) || Findable.config.seed_dir
6
- raise ArgumentError unless target_dir
7
- Pathname.glob(target_dir.join("**", "*")).map {|full_path|
8
- new(full_path, target_dir)
9
- }.select {|seed|
10
- seed_files.present? ? seed_files.include?(seed.basename) : true
11
- }
18
+ target_dir = pathname(seed_dir || Findable.config.seed_dir)
19
+ raise UnknownSeedDir.new(target_dir) unless target_dir.try(:exist?)
20
+
21
+ seed_files = seed_files.map!(&:to_s) if seed_files
22
+ _model_name = method(:model_name).to_proc.curry.call(target_dir)
23
+ _selected = Proc.new do |seed|
24
+ seed_files.present? ? seed.table_name.in?(seed_files) : true
25
+ end
26
+
27
+ Pathname.glob(target_dir.join("**", "*"))
28
+ .select(&:file?)
29
+ .group_by(&_model_name)
30
+ .map {|name, files| new(files, name) }
31
+ .select(&_selected)
32
+ end
33
+
34
+ def model_name(seed_dir, seed_file)
35
+ if seed_dir != seed_file.dirname && seed_file.basename.to_s.match(/^data/)
36
+ from_seed_dir(seed_dir, seed_file.dirname).to_s.classify
37
+ else
38
+ from_seed_dir(seed_dir, without_ext(seed_file)).to_s.classify
39
+ end
40
+ end
41
+
42
+ def from_seed_dir(seed_dir, seed_file)
43
+ pathname(seed_file).relative_path_from(seed_dir)
44
+ end
45
+
46
+ def without_ext(seed_file)
47
+ pathname(seed_file).dirname.join(pathname(seed_file).basename(".*"))
12
48
  end
13
49
 
14
50
  def pathname(path)
@@ -20,37 +56,35 @@ module Findable
20
56
  end
21
57
  end
22
58
 
23
- def initialize(full_path, seed_dir)
24
- @_full_path = full_path
25
- @_seed_dir = seed_dir
26
- end
27
-
28
- def basename
29
- @_basename ||= @_full_path.basename(".*").to_s
59
+ def model
60
+ @_model_class ||= model_name.constantize
30
61
  end
31
62
 
32
- def model
33
- @_model ||= from_seed_dir(without_ext(@_full_path)).to_s.classify.constantize
63
+ def load_files
64
+ seed_files.sort_by(&:to_s).inject([]) do |data, file|
65
+ data | case file.extname
66
+ when ".yml" then load_yaml(file)
67
+ when ".csv" then load_csv(file)
68
+ else
69
+ raise UnknownFormat
70
+ end
71
+ end
34
72
  end
35
73
 
36
74
  def bootstrap!
37
75
  model.query.lock do
38
76
  model.delete_all
39
- model.query.import YAML.load_file(@_full_path).values
77
+ model.query.import load_files
40
78
  end
41
79
  end
42
80
 
43
81
  private
44
- def pathname(path)
45
- self.class.pathname(path)
46
- end
47
-
48
- def without_ext(path)
49
- pathname(path).dirname.join(pathname(path).basename(".*"))
82
+ def load_yaml(seed_file)
83
+ YAML.load_file(seed_file).values
50
84
  end
51
85
 
52
- def from_seed_dir(path)
53
- pathname(path).relative_path_from(@_seed_dir)
86
+ def load_csv(seed_file)
87
+ CSV.table(seed_file).map(&:to_h)
54
88
  end
55
89
  end
56
90
  end
@@ -1,3 +1,3 @@
1
1
  module Findable
2
- VERSION = "0.2.1"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -10,4 +10,4 @@ require "findable/seed"
10
10
  seed_files = nil
11
11
 
12
12
  # Execute
13
- Findable::Seed.target_files(seed_files: seed_files).each {|seed| seed.bootstrap! }
13
+ Findable::Seed.target_files(seed_files: seed_files).each(&:bootstrap!)
@@ -125,12 +125,12 @@ describe Findable::Base do
125
125
 
126
126
  # Private instance methods
127
127
  describe "#attribute=" do
128
- before { instance.send(:attribute=, :example, "value") }
129
- it { expect(instance.attributes[:example]).to eq("value") }
128
+ before { instance.send(:attribute=, :name, "value") }
129
+ it { expect(instance.attributes[:name]).to eq("value") }
130
130
  end
131
131
 
132
132
  describe "#attribute?" do
133
- before { instance.send(:attribute=, :example, "value") }
134
- it { expect(instance.send(:attribute?, :example)).to be_truthy }
133
+ before { instance.send(:attribute=, :name, "value") }
134
+ it { expect(instance.send(:attribute?, :name)).to be_truthy }
135
135
  end
136
136
  end
@@ -52,7 +52,7 @@ describe Findable::Query do
52
52
 
53
53
  it {
54
54
  expect {
55
- model.query.delete(persisted_object.id)
55
+ model.query.delete(persisted_object)
56
56
  }.to change { model.query.count }.by(-1)
57
57
  }
58
58
  end
@@ -4,17 +4,17 @@ describe Findable::Schema::Conversion do
4
4
  after(:each) { conversion.clear_types }
5
5
  let(:conversion) { Findable::Schema::Conversion }
6
6
 
7
- describe ".for" do
8
- it { expect(conversion.for(nil)).to eq(conversion.types[:default]) }
9
- it { expect(conversion.for(:integer)).to eq(conversion.types[:integer]) }
10
- it { expect(conversion.for(:float)).to eq(conversion.types[:float]) }
11
- it { expect(conversion.for(:decimal)).to eq(conversion.types[:decimal]) }
12
- it { expect(conversion.for(:string)).to eq(conversion.types[:string]) }
13
- it { expect(conversion.for(:boolean)).to eq(conversion.types[:boolean]) }
14
- it { expect(conversion.for(:date)).to eq(conversion.types[:date]) }
15
- it { expect(conversion.for(:datetime)).to eq(conversion.types[:datetime]) }
16
- it { expect(conversion.for(:symbol)).to eq(conversion.types[:symbol]) }
17
- it { expect(conversion.for(:inquiry)).to eq(conversion.types[:inquiry]) }
7
+ describe ".to" do
8
+ it { expect(conversion.to(nil)).to eq(conversion.types[:default]) }
9
+ it { expect(conversion.to(:integer)).to eq(conversion.types[:integer]) }
10
+ it { expect(conversion.to(:float)).to eq(conversion.types[:float]) }
11
+ it { expect(conversion.to(:decimal)).to eq(conversion.types[:decimal]) }
12
+ it { expect(conversion.to(:string)).to eq(conversion.types[:string]) }
13
+ it { expect(conversion.to(:boolean)).to eq(conversion.types[:boolean]) }
14
+ it { expect(conversion.to(:date)).to eq(conversion.types[:date]) }
15
+ it { expect(conversion.to(:datetime)).to eq(conversion.types[:datetime]) }
16
+ it { expect(conversion.to(:symbol)).to eq(conversion.types[:symbol]) }
17
+ it { expect(conversion.to(:inquiry)).to eq(conversion.types[:inquiry]) }
18
18
  end
19
19
 
20
20
  describe ".types" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: findable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - i2bskn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-15 00:00:00.000000000 Z
11
+ date: 2015-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport