findable 0.2.1 → 0.2.2

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 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