ii_finder 1.1.2 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e833c71166437a7e1f0881641850c3b2bc6f36f9c7827a2e18d0c63ba9d3e4b
4
- data.tar.gz: 6f653a027ee5947a6c7bbcd0ce3f76999513f334b79ebd5a8b12dc907961ea2f
3
+ metadata.gz: e9c42e9402b1850eb69b928c7ea83e978e3ccbe3b3356eb4e5b1d24edf9424ce
4
+ data.tar.gz: d22e81c3f0c1bd32b9637d8722b5c6007ae5bf2c27c9b880c2c3b90e88f23363
5
5
  SHA512:
6
- metadata.gz: 1b861518b33b016ccc5795cd04c6c1677a159efd43eeb023bb00ae068b00cb2ca2820c0bf35c58b136cf6ee8392995b87b4a4d5e6e8bbfc5e09f1ab7e16a422c
7
- data.tar.gz: 443dc16e940192156b4af6af1747d9f81166455870ed50215a361c61b9e0535b6ae60e14a0824ae989be6cb141e844235cec5cfbb2b210e4923694b9e792cd73
6
+ metadata.gz: d5d8a31523c9cfa994a683c51723bbb62a1ca09891f1653ccc7f4af8a0b4a28a6927983886c504b86bfbb22e331f50e103f5378b5931e60e4111c49219e8670e
7
+ data.tar.gz: 1c0770784cc663a8bcba3c99b1d0af1439d29c23b8b5a885f7cc34a5c06136972879a9ce8d48d619ca4b6de80639f76c4eb4d51749597a619032f4d1324b54ed
@@ -4,21 +4,29 @@ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  test:
7
- runs-on: ubuntu-18.04
7
+ runs-on: ubuntu-20.04
8
8
  strategy:
9
9
  fail-fast: false
10
10
  matrix:
11
- ruby: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
12
- gemfile: ['rails50', 'rails51', 'rails52', 'rails60', 'rails61']
11
+ ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0']
12
+ gemfile: ['rails50', 'rails51', 'rails52', 'rails60', 'rails61', 'rails70']
13
13
  exclude:
14
14
  - ruby: 2.3
15
15
  gemfile: rails60
16
16
  - ruby: 2.3
17
17
  gemfile: rails61
18
+ - ruby: 2.3
19
+ gemfile: rails70
18
20
  - ruby: 2.4
19
21
  gemfile: rails60
20
22
  - ruby: 2.4
21
23
  gemfile: rails61
24
+ - ruby: 2.4
25
+ gemfile: rails70
26
+ - ruby: 2.5
27
+ gemfile: rails70
28
+ - ruby: 2.6
29
+ gemfile: rails70
22
30
  - ruby: 3.0
23
31
  gemfile: rails50
24
32
  - ruby: 3.0
data/CHANGELOG.md CHANGED
@@ -1,8 +1,28 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.1.0
4
+
5
+ * Add traversal config.
6
+ * Add callbacks for `call_all`.
7
+ * Add calling instrumentation.
8
+ * Clear class cache when reloaded.
9
+ * Bump coactive version to 0.2.
10
+
11
+ ## 2.0.1
12
+
13
+ * Return relation after callbacks called.
14
+
15
+ ## 2.0.0
16
+
17
+ * Replace chain feature with coactive.
18
+
19
+ ## 1.2.0
20
+
21
+ * Add chain feature.
22
+
3
23
  ## 1.1.2
4
24
 
5
- Try fetch prior to calling instance method.
25
+ * Try fetch prior to calling instance method.
6
26
 
7
27
  ## 1.1.1
8
28
 
data/README.md CHANGED
@@ -24,14 +24,14 @@ Then execute:
24
24
  Prepare model:
25
25
 
26
26
  ```ruby
27
- class User < ActiveRecord::Base
27
+ class Item < ActiveRecord::Base
28
28
  end
29
29
  ```
30
30
 
31
31
  Prepare finder:
32
32
 
33
33
  ```ruby
34
- class UsersFinder < IIFinder::Base
34
+ class ItemsFinder < IIFinder::Base
35
35
  parameters :name
36
36
 
37
37
  def name(value)
@@ -43,15 +43,15 @@ end
43
43
  Use finder as follows:
44
44
 
45
45
  ```ruby
46
- UsersFinder.call(name: 'NAME').to_sql
47
- #=> SELECT "users".* FROM "users" WHERE "users"."name" = 'NAME'
46
+ ItemsFinder.call(name: 'NAME').to_sql
47
+ #=> SELECT "items".* FROM "items" WHERE "items"."name" = 'NAME'
48
48
  ```
49
49
 
50
50
  You can also specify relation as first argument:
51
51
 
52
52
  ```ruby
53
- UsersFinder.call(User.where(id: [1, 2, 3]), name: 'NAME').to_sql
54
- #=> SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3) AND "users"."name" = 'NAME'
53
+ ItemsFinder.call(Item.where(id: [1, 2, 3]), name: 'NAME').to_sql
54
+ #=> SELECT "items".* FROM "items" WHERE "items"."id" IN (1, 2, 3) AND "items"."name" = 'NAME'
55
55
  ```
56
56
 
57
57
  ### Finder
@@ -61,7 +61,7 @@ Finder method will not be called when the value of parameter is blank.
61
61
  If you want to receive such value, set `allow_blank` as follows:
62
62
 
63
63
  ```ruby
64
- class UsersFinder < IIFinder::Base
64
+ class ItemsFinder < IIFinder::Base
65
65
  parameters :name, allow_blank: true
66
66
 
67
67
  def name(value)
@@ -69,14 +69,14 @@ class UsersFinder < IIFinder::Base
69
69
  end
70
70
  end
71
71
 
72
- UsersFinder.call(name: '').to_sql
73
- #=> SELECT "users".* FROM "users" WHERE "users"."name" = ''
72
+ ItemsFinder.call(name: '').to_sql
73
+ #=> SELECT "items".* FROM "items" WHERE "items"."name" = ''
74
74
  ```
75
75
 
76
76
  Finder has following attributes:
77
77
 
78
78
  ```ruby
79
- class UsersFinder < IIFinder::Base
79
+ class ItemsFinder < IIFinder::Base
80
80
  parameters :name
81
81
 
82
82
  def name(value)
@@ -87,10 +87,10 @@ class UsersFinder < IIFinder::Base
87
87
  end
88
88
  end
89
89
 
90
- UsersFinder.call(name: 'NAME')
91
- #=> relation: #<User::ActiveRecord_Relation:
90
+ ItemsFinder.call(name: 'NAME')
91
+ #=> relation: #<Item::ActiveRecord_Relation:
92
92
  # criteria: {:name=>'NAME'}
93
- # model: User
93
+ # model: Item
94
94
  # table: #<Arel::Table ...>
95
95
  ```
96
96
 
@@ -102,7 +102,7 @@ IIFinder.configure do |config|
102
102
  config.merge_relation = false
103
103
  end
104
104
 
105
- class UsersFinder < IIFinder::Base
105
+ class ItemsFinder < IIFinder::Base
106
106
  parameters :name
107
107
 
108
108
  def name(value)
@@ -110,8 +110,8 @@ class UsersFinder < IIFinder::Base
110
110
  end
111
111
  end
112
112
 
113
- UsersFinder.call(name: 'NAME').to_sql
114
- #=> SELECT "users".* FROM "users" WHERE "users"."name" = 'NAME'
113
+ ItemsFinder.call(name: 'NAME').to_sql
114
+ #=> SELECT "items".* FROM "items" WHERE "items"."name" = 'NAME'
115
115
  ```
116
116
 
117
117
  #### Callbacks
@@ -125,7 +125,7 @@ Following callbacks are available.
125
125
  For example:
126
126
 
127
127
  ```ruby
128
- class UsersFinder < IIFinder::Base
128
+ class ItemsFinder < IIFinder::Base
129
129
  after_call :default_order
130
130
 
131
131
  def default_order
@@ -133,14 +133,45 @@ class UsersFinder < IIFinder::Base
133
133
  end
134
134
  end
135
135
 
136
- UsersFinder.call.to_sql
137
- #=> SELECT "users".* FROM "users" ORDER BY "users"."id" DESC
136
+ ItemsFinder.call.to_sql
137
+ #=> SELECT "items".* FROM "items" ORDER BY "items"."id" DESC
138
138
  ```
139
139
 
140
140
  Note that finder does not handle the return value of callback.
141
141
  When you want to update `@relation` in the callback,
142
142
  reassign `@relation` or use methods like `where!` or `order!`.
143
143
 
144
+ #### Coactors
145
+
146
+ You can chain multiple finders by using `coact`. For example:
147
+
148
+ ```ruby
149
+ class NameFinder < IIFinder::Base
150
+ parameters :name
151
+
152
+ def name(value)
153
+ @relation.where(name: value)
154
+ end
155
+ end
156
+
157
+ class AgeFinder < IIFinder::Base
158
+ parameters :age
159
+
160
+ def age(value)
161
+ @relation.where(age: value)
162
+ end
163
+ end
164
+
165
+ class ItemsFinder < IIFinder::Base
166
+ coact NameFinder, AgeFinder
167
+ end
168
+
169
+ ItemsFinder.call(Item.all, name: 'name', age: 10).to_sql
170
+ #=> SELECT "items".* FROM "items" WHERE "items"."name" = 'name' AND "items"."age" = 10
171
+ ```
172
+
173
+ See [coactive](https://github.com/kanety/coactive) for more `coact` examples:
174
+
144
175
  ### Lookup for model
145
176
 
146
177
  Finder lookups related model by its class name when the first argument of `call` is not relation.
@@ -148,30 +179,30 @@ So the name of finder class should be composed of the name of model class.
148
179
  For example:
149
180
 
150
181
  ```ruby
151
- class User < ActiveRecord::Base
182
+ class Item < ActiveRecord::Base
152
183
  end
153
184
 
154
- class UsersFinder < IIFinder::Base
185
+ class ItemsFinder < IIFinder::Base
155
186
  end
156
187
 
157
- IIFinder::Base.lookup(UsersFinder)
158
- #=> User
188
+ IIFinder::Base.lookup(ItemsFinder)
189
+ #=> Item
159
190
  ```
160
191
 
161
192
  Note that superclass of finder is also looked up until related model is found.
162
193
 
163
194
  ```ruby
164
- class User < ActiveRecord::Base
195
+ class Item < ActiveRecord::Base
165
196
  end
166
197
 
167
- class UsersFinder < IIFinder::Base
198
+ class ItemsFinder < IIFinder::Base
168
199
  end
169
200
 
170
- class InheritedUsersFinder < UsersFinder
201
+ class InheritedItemsFinder < ItemsFinder
171
202
  end
172
203
 
173
- IIFinder::Base.lookup(InheritedUsersFinder)
174
- #=> User
204
+ IIFinder::Base.lookup(InheritedItemsFinder)
205
+ #=> Item
175
206
  ```
176
207
 
177
208
  ### Scope for model
@@ -179,12 +210,12 @@ IIFinder::Base.lookup(InheritedUsersFinder)
179
210
  In case you want to call finder from model, include `IIFinder::Scope` into model as follows:
180
211
 
181
212
  ```ruby
182
- class User < ActiveRecord::Base
213
+ class Item < ActiveRecord::Base
183
214
  include IIFinder::Scope
184
215
  end
185
216
 
186
- User.finder_scope(name: 'NAME').to_sql
187
- #=> SELECT "users".* FROM "users" WHERE "users"."name" = 'NAME'
217
+ Item.finder_scope(name: 'NAME').to_sql
218
+ #=> SELECT "items".* FROM "items" WHERE "items"."name" = 'NAME'
188
219
  ```
189
220
 
190
221
  ### Logging
@@ -199,7 +230,9 @@ IIFinder::LogSubscriber.attach_to :ii_finder
199
230
  This subscriber will write logs in debug mode as the following example:
200
231
 
201
232
  ```
202
- Called UsersFinder with {:id=>1} (Duration: 9.9ms, Allocations: 915)
233
+ Calling ItemsFinder with {:id=>1}
234
+ ...
235
+ Called ItemsFinder (Duration: 9.9ms, Allocations: 915)
203
236
  ```
204
237
 
205
238
  ## Contributing
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "rails", "~> 7.0.0"
4
+
5
+ gemspec path: "../"
data/ii_finder.gemspec CHANGED
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.require_paths = ["lib"]
19
19
 
20
20
  spec.add_dependency "activesupport", ">= 5.0"
21
+ spec.add_dependency "coactive", ">= 0.2"
21
22
 
22
23
  spec.add_development_dependency "rails", ">= 5.0"
23
24
  spec.add_development_dependency "sqlite3"
@@ -5,6 +5,9 @@ require_relative 'parameters'
5
5
  require_relative 'callbacks'
6
6
  require_relative 'instrumentation'
7
7
  require_relative 'lookup'
8
+ require_relative 'context'
9
+ require_relative 'contextualizer'
10
+ require_relative 'coactors'
8
11
 
9
12
  module IIFinder
10
13
  class Base
@@ -13,5 +16,7 @@ module IIFinder
13
16
  include Callbacks
14
17
  include Instrumentation
15
18
  include Lookup
19
+ include Contextualizer
20
+ include Coactors
16
21
  end
17
22
  end
@@ -6,9 +6,16 @@ module IIFinder
6
6
  include ActiveSupport::Callbacks
7
7
 
8
8
  included do
9
+ define_callbacks :all
9
10
  define_callbacks :call
10
11
  end
11
12
 
13
+ def call_all
14
+ run_callbacks :all do
15
+ super
16
+ end
17
+ end
18
+
12
19
  def call
13
20
  run_callbacks :call do
14
21
  super
@@ -16,6 +23,18 @@ module IIFinder
16
23
  end
17
24
 
18
25
  class_methods do
26
+ def before_all(*args, &block)
27
+ set_callback(:all, :before, *args, &block)
28
+ end
29
+
30
+ def after_all(*args, &block)
31
+ set_callback(:all, :after, *args, &block)
32
+ end
33
+
34
+ def around_all(*args, &block)
35
+ set_callback(:all, :around, *args, &block)
36
+ end
37
+
19
38
  def before_call(*args, &block)
20
39
  set_callback(:call, :before, *args, &block)
21
40
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IIFinder
4
+ module Coactors
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include Coactive::Base
9
+
10
+ configure_coactive do |config|
11
+ config.load_paths = ['app/finders']
12
+ config.class_suffix = 'Finder'
13
+ config.use_cache = true
14
+ config.lookup_superclass_until = ['ActiveRecord::Base', 'ActiveModel::Base']
15
+ end
16
+
17
+ class << self
18
+ alias_method :chain, :coact
19
+ end
20
+ end
21
+ end
22
+ end
@@ -6,7 +6,8 @@ module IIFinder
6
6
 
7
7
  self.data = {
8
8
  lookup_cache: true,
9
- merge_relation: true
9
+ merge_relation: true,
10
+ traversal: :postorder,
10
11
  }
11
12
 
12
13
  data.keys.each do |key|
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IIFinder
4
+ class Context < Coactive::Context
5
+ def to_s
6
+ "#<#{self.class} model=#{@_data[:model]} criteria=#{@_data[:criteria].to_s.truncate(300)}>"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IIFinder
4
+ module Contextualizer
5
+ extend ActiveSupport::Concern
6
+ include Coactive::Contextualizer
7
+
8
+ def call
9
+ contextualize do
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
@@ -3,34 +3,49 @@
3
3
  module IIFinder
4
4
  module Core
5
5
  extend ActiveSupport::Concern
6
+ include Coactive::Initializer
6
7
 
7
8
  included do
8
- attr_reader :relation, :criteria, :model, :table
9
+ self.context_class = IIFinder::Context
10
+ context :relation, output: true
11
+ context :criteria, :model, :table
9
12
  end
10
13
 
11
14
  def initialize(*args)
12
- if args.size == 0 || args.size == 1
13
- @model = self.class.lookup
14
- raise IIFinder::Error.new("could not find model for #{self.class}") unless @model
15
- @relation = @model.all
16
- @criteria = args[0] || {}
15
+ if args[0].is_a?(self.class.context_class)
16
+ super(args[0])
17
17
  else
18
- @relation = args[0]
19
- @criteria = args[1]
20
- @model = @relation.klass
18
+ relation, criteria = Core.resolve_args(self, *args)
19
+ model = relation.klass
20
+ table = model.arel_table if model.respond_to?(:arel_table)
21
+ super(relation: relation, criteria: criteria, model: model, table: table)
22
+ end
23
+ end
24
+
25
+ def call_all
26
+ planned = case IIFinder.config.traversal
27
+ when :preorder
28
+ [self] + coactors
29
+ when :postorder
30
+ coactors + [self]
31
+ when :inorder
32
+ planned = coactors.in_groups(2, false)
33
+ planned[0] + [self] + planned[1]
34
+ end
35
+
36
+ planned.each do |finder|
37
+ relation = finder == self ? call : finder.call(@context)
38
+ @context.relation = @context.relation.merge(relation) if relation.respond_to?(:merge)
21
39
  end
22
- @table = @model.arel_table if @model.respond_to?(:arel_table)
23
40
  end
24
41
 
25
42
  def call
26
43
  self.class._parameters.each do |param|
27
44
  value = fetch_criteria(param.name)
28
45
  if value.present? || param.allow_blank?
29
- call_method(param.name, value)
46
+ merge_relation!(send(param.name, value))
30
47
  end
31
48
  end
32
-
33
- @relation
34
49
  end
35
50
 
36
51
  def fetch_criteria(name)
@@ -41,17 +56,28 @@ module IIFinder
41
56
  end
42
57
  end
43
58
 
44
- def call_method(name, value)
45
- result = send(name, value)
46
-
47
- if result.respond_to?(:merge) && Config.merge_relation
48
- @relation = @relation.merge(result)
59
+ def merge_relation!(relation)
60
+ if relation.respond_to?(:merge) && Config.merge_relation
61
+ @relation = @relation.merge(relation)
49
62
  end
50
63
  end
51
64
 
52
65
  class_methods do
53
66
  def call(*args)
54
- new(*args).call
67
+ finder = new(*args).tap(&:call_all)
68
+ finder.context.relation
69
+ end
70
+ end
71
+
72
+ class << self
73
+ def resolve_args(finder, *args)
74
+ if args.size == 0 || args.size == 1
75
+ model = finder.class.lookup
76
+ raise IIFinder::Error.new("could not find model for #{finder.class}") unless model
77
+ return model.all, args[0] || {}
78
+ else
79
+ return args[0], args[1]
80
+ end
55
81
  end
56
82
  end
57
83
  end
@@ -4,6 +4,11 @@ module IIFinder
4
4
  module Instrumentation
5
5
  extend ActiveSupport::Concern
6
6
 
7
+ def call_all
8
+ ActiveSupport::Notifications.instrument 'calling.ii_finder', finder: self
9
+ super
10
+ end
11
+
7
12
  def call
8
13
  ActiveSupport::Notifications.instrument 'call.ii_finder', finder: self do
9
14
  super
@@ -2,13 +2,22 @@
2
2
 
3
3
  module IIFinder
4
4
  class LogSubscriber < ActiveSupport::LogSubscriber
5
+ def calling(event)
6
+ debug do
7
+ finder = event.payload[:finder]
8
+ "Calling #{finder.class} with #{finder.context}"
9
+ end
10
+ end
11
+
5
12
  def call(event)
6
13
  debug do
7
14
  finder = event.payload[:finder]
8
- "Called #{finder.class} with #{finder.criteria} (#{additional_log(event)})"
15
+ "Called #{finder.class} (#{additional_log(event)})"
9
16
  end
10
17
  end
11
18
 
19
+ private
20
+
12
21
  def additional_log(event)
13
22
  additions = ["Duration: %.1fms" % event.duration]
14
23
  additions << "Allocations: %d" % event.allocations if event.respond_to?(:allocations)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IIFinder
4
+ class Railtie < Rails::Railtie
5
+ ActiveSupport::Reloader.to_prepare do
6
+ IIFinder::Lookup.cache.clear
7
+ end
8
+ end
9
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module IIFinder
4
- VERSION = '1.1.2'
4
+ VERSION = '2.1.0'
5
5
  end
data/lib/ii_finder.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'active_support'
2
+ require 'coactive'
2
3
 
3
4
  require 'ii_finder/version'
4
5
  require 'ii_finder/config'
@@ -6,6 +7,7 @@ require 'ii_finder/errors'
6
7
  require 'ii_finder/base'
7
8
  require 'ii_finder/scope'
8
9
  require 'ii_finder/log_subscriber'
10
+ require 'ii_finder/railtie' if defined?(Rails)
9
11
 
10
12
  module IIFinder
11
13
  class << self
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ii_finder
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoshikazu Kaneta
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-25 00:00:00.000000000 Z
11
+ date: 2022-01-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: coactive
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rails
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -116,11 +130,15 @@ files:
116
130
  - gemfiles/rails52.gemfile
117
131
  - gemfiles/rails60.gemfile
118
132
  - gemfiles/rails61.gemfile
133
+ - gemfiles/rails70.gemfile
119
134
  - ii_finder.gemspec
120
135
  - lib/ii_finder.rb
121
136
  - lib/ii_finder/base.rb
122
137
  - lib/ii_finder/callbacks.rb
138
+ - lib/ii_finder/coactors.rb
123
139
  - lib/ii_finder/config.rb
140
+ - lib/ii_finder/context.rb
141
+ - lib/ii_finder/contextualizer.rb
124
142
  - lib/ii_finder/core.rb
125
143
  - lib/ii_finder/errors.rb
126
144
  - lib/ii_finder/instrumentation.rb
@@ -128,6 +146,7 @@ files:
128
146
  - lib/ii_finder/lookup.rb
129
147
  - lib/ii_finder/parameter.rb
130
148
  - lib/ii_finder/parameters.rb
149
+ - lib/ii_finder/railtie.rb
131
150
  - lib/ii_finder/scope.rb
132
151
  - lib/ii_finder/version.rb
133
152
  homepage: https://github.com/kanety/ii_finder