activerecord-bixformer 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: d52b22718a4ac2c5ccff247f0df35fe225aea4d4
4
+ data.tar.gz: a6f59a2546219966f72e88c371fad86f480096e9
5
+ SHA512:
6
+ metadata.gz: 4fcb3e7796d7c9bc77c8fc3bfaabc49dff5cf85af85df0782da392f78c163d1533ce2aafca26a2285ceb9d1a6776c44e32d2647deb79cd7dd4576a28d259e84a
7
+ data.tar.gz: 93733cc2ecea781fd6e536d38968f437d36f25a97a50fe4f5eae531c4279bfd47c59bf337c0ce8ff353edf96307687a906cb2c2929959ff9b31e2b37baf13436
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format
3
+ doc
4
+ --backtrace
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activerecord-bixformer.gemspec
4
+ gemspec
data/README-ja.md ADDED
@@ -0,0 +1,296 @@
1
+ # コレは何?
2
+
3
+ データベースのレコードを ActiveRecord を介して、他のデータ形式にインポート/エクスポートする処理をサポートするフレームワークです。
4
+
5
+ 以下のような特徴があります。
6
+
7
+ * `accepts_nested_attributes_for` で定義された関連モデルを同時にインポート/エクスポート可能
8
+ * インポートデータに `primary_key` が含まれる場合、それが正当な値かどうかをチェック
9
+ * 設定用クラスを定義して使い、モデル、属性単位で処理をカスタマイズ可能
10
+ * その設定用クラスを複数定義して、実行時に切り替え可能
11
+
12
+ 現在のところ、インポート/エクスポート可能なデータ形式は
13
+
14
+ * CSV
15
+
16
+ です。
17
+
18
+ ## インストール
19
+
20
+ Gemfile に
21
+
22
+ ```ruby
23
+ gem 'activerecord-bixformer'
24
+ ```
25
+
26
+ と記述し、
27
+
28
+ $ bundle
29
+
30
+ とするか、もしくは、
31
+
32
+ $ gem install activerecord-bixformer
33
+
34
+ ## 構成要素
35
+
36
+ 本フレームワークの構成要素と役割は以下のようになっています。
37
+
38
+ ### Modeler
39
+
40
+ インポート/エクスポートの一連の処理をどのように処理するかを定義する設定ファイルの役割です。
41
+ 処理実行時に、複数登録された本クラスのインスタンス群から採用するものを決定するフローになっていて、
42
+ バージョンや条件による処理内容の切り替えを容易にできます。
43
+
44
+ ### Runner
45
+
46
+ インポート/エクスポートの一連の処理を実行するクラスです。
47
+ 本クラスをそのまま利用するだけでも、インポート/エクスポートが可能です。
48
+
49
+ ### Model
50
+
51
+ ActiveRecord のモデルに対応して、そのモデルのインポート/エクスポート処理を担当するクラスです。
52
+
53
+ ActiveRecord::Bixformer::Model::Base を継承した独自クラスを定義し、
54
+ Modelerを適切に定義することで、それを使用して処理内容を切り替えることができます。
55
+
56
+ ### Attribute
57
+
58
+ ActiveRecord のモデルの持つ属性に対応して、その属性のインポート/エクスポート処理を担当するクラスです。
59
+
60
+ ActiveRecord::Bixformer::Attribute::Base を継承した独自クラスを定義し、
61
+ Modelerを適切に定義することで、それを使用して処理内容を切り替えることができます。
62
+
63
+ # 使い方
64
+
65
+ ## 1. Modelerの実装
66
+
67
+ ActiveRecord::Bixformer::Modeler::Base を継承して、以下のメソッドを適切に設定して下さい。
68
+
69
+ ### model_name
70
+
71
+ インポート/エクスポート対象のモデル名を返して下さい。
72
+ 例えばusersテーブルのデータが対象であれば、 `:user` です。
73
+
74
+ ### entry_definition
75
+
76
+ インポート/エクスポート対象の属性と、それをどのように処理するかを定義した以下のようなハッシュを返して下さい。
77
+
78
+ ```ruby
79
+ {
80
+ # 対象モデル(上の例なら user )の処理を担当するModelクラス名
81
+ # 省略可能で、省略された場合は :base が指定される
82
+ # 指定可能な値については、「本フレームワークに定義されたModel一覧」を参照
83
+ # または、独自クラスを定義し、 module_load_namespaces を設定
84
+ type: :base,
85
+
86
+ # 処理対象の属性名をキー、その処理を担当するAttributeクラス名を値に持つハッシュ
87
+ attributes: {
88
+ # 指定可能な値については、「本フレームワークに定義されたAttribute一覧」を参照
89
+ # または、独自クラスを定義し、 module_load_namespaces を設定
90
+ name: :base,
91
+ # クラス名を配列にすると、2番目以降の要素はAttributeクラスに渡される
92
+ joined_at: [:time, format: :ymdhms]
93
+ },
94
+
95
+ # 処理対象の関連名をキー、その処理を定義したハッシュを値に持つハッシュ
96
+ associations: {
97
+ posts: {
98
+ # 同じように、 type, attributes, associations が定義可能
99
+
100
+ # 配列にすると、2番目以降の要素はModelクラスに渡される
101
+ type: [:indexed, size: 3]
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### optional_attributes
108
+
109
+ インポート時に、有効な値でない場合に、登録対象としない属性を定義した以下のような配列を返して下さい。
110
+
111
+ ```ruby
112
+ [
113
+ # 対象モデル(上の例なら user )の属性名。 entry_definition で定義されていること
114
+ :name,
115
+
116
+ # 関連名も指定可能。この場合は、その関連モデルの処理対象の属性全てが有効な値でない場合
117
+ :posts,
118
+
119
+ # 関連モデルの持つ要素を個別指定したい場合は、ハッシュで定義
120
+ posts: [
121
+ # 同じように定義可能
122
+ :title
123
+ ]
124
+ ]
125
+ ```
126
+
127
+ #### 有効な値
128
+
129
+ 有効な値かどうかの判定は、現在、以下の実装となっています。
130
+
131
+ ```ruby
132
+ def presence_value?(value)
133
+ case value
134
+ when ::Hash
135
+ value.values.any? { |v| presence_value?(v) }
136
+ when ::Array
137
+ value.any? { |v| presence_value?(v) }
138
+ when ::String
139
+ ! value.blank?
140
+ when ::TrueClass, ::FalseClass
141
+ true
142
+ else
143
+ value ? true : false
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### required_attributes
149
+
150
+ インポート時に、有効な値でない場合に、インポート自体を行わない属性を定義した配列を返して下さい。
151
+ データ構成は、 `optional_attributes` と同様です。
152
+
153
+ ### unique_indexes
154
+
155
+ インポートは、対象の ActiveRecord モデルの `primary_key` に対応するインポートデータの有無によって、
156
+ 追加か更新かを判定しますが、 `primary_key` の属性がインポートデータに含まれていない場合でも、
157
+ 更新処理を行いたい場合に、対象レコードを特定できる属性を定義した配列を返して下さい。
158
+ データ構成は、 `optional_attributes` と同様です。
159
+
160
+ ```ruby
161
+ [
162
+ :name, :birthday,
163
+ posts: [:user_id, :title]
164
+ ]
165
+ ```
166
+
167
+ * 上記の場合、 user は name, birthday で特定され、 user.posts は user_id, title で特定されます
168
+ * foreign_key(上記の場合、 user_id )は、インポートデータの有無に関わらず、データベースの正しい値で補完されます
169
+ * それ以外のもので、インポートデータに有効な値がなかった場合は、更新はせず、追加になります
170
+ * モデルやDBに、実際にユニークインデックスが定義されていなくても指定可能です
171
+ * ただし、その場合は、条件に合致したレコードが複数あった場合、どれが更新されるか保証できません
172
+
173
+ ### required_condition
174
+
175
+ インポート時に、 `model_name` のインポートデータに追加する条件を定義した以下のようなハッシュを返して下さい。
176
+
177
+ ```ruby
178
+ {
179
+ # 対象モデル(上の例なら user )は、現在処理対象になっている group に属している
180
+ group_id: current_group.id
181
+ }
182
+ ```
183
+
184
+ * `primary_key` や `unique_indexes` が指定されている場合のデータベース検索の条件に追加されます
185
+ * 関連モデルの場合は、親レコードの foreign_key が代わりに使用されます
186
+ * `primary_key` が指定されているのに、データベース検索に失敗した場合には、 `ActiveRecord::RecordNotFound` 例外が raise されます
187
+
188
+ ### default_values
189
+
190
+ インポート時に、有効な値でない場合に、代わりにインポートする値を定義した以下のようなハッシュを返して下さい。
191
+
192
+ ```ruby
193
+ {
194
+ name: '名無しの権兵衛',
195
+ posts: {
196
+ title: '無題'
197
+ }
198
+ }
199
+ ```
200
+
201
+ ### translation_config
202
+
203
+ インポート/エクスポートで行われる translation の設定を定義した以下のようなハッシュを返して下さい。
204
+
205
+ ```ruby
206
+ {
207
+ # 基点のスコープ
208
+ scope: :bixformer,
209
+
210
+ # translation を試みるスコープを、基点のスコープ配下に増やしたい場合に指定
211
+ extend_scopes: [:version1, :version2]
212
+ }
213
+ ```
214
+
215
+ 上記の場合、ユーザが投稿したタイトルは
216
+
217
+ `bixformer.version2.user/posts.title`
218
+ `bixformer.version1.user/posts.title`
219
+ `bixformer.user/posts.title`
220
+
221
+ の順で検索されます。translation が見つからなかった場合は、エラーになります。
222
+
223
+ * CSVでは
224
+ * カラム名に使用されます
225
+
226
+ ### module_load_namespaces
227
+
228
+ `entry_definition` で指定されたクラス名のクラスを探索する namespace を定義した配列を返して下さい。
229
+ ActiveRecord::Bixformer::Modeler::Base には、以下のように定義されています。
230
+
231
+ ```ruby
232
+ def module_load_namespaces(module_type)
233
+ [
234
+ "::ActiveRecord::Bixformer::#{module_type.to_s.camelize}::#{format.to_s.camelize}",
235
+ "::ActiveRecord::Bixformer::#{module_type.to_s.camelize}",
236
+ ]
237
+ end
238
+ ```
239
+
240
+ * 要素の先頭から、 `要素::クラス名.to_s.classify.constantize` を試し、成功したものを採用します
241
+ * `module_type` には、 `:model` / `:attribute` / `:generator` のいずれかが渡されます
242
+ * `format` は、対象のデータ形式( `:csv` )になります
243
+
244
+ ## 2. Runner を実装
245
+
246
+ CSVを扱う簡単なサンプルコードは以下のような感じになります。
247
+
248
+ ```ruby
249
+ runner = ActiveRecord::Bixformer::Runner::Csv.new
250
+
251
+ runner.add_modeler(Your::Modeler.new)
252
+
253
+ csv_data = runner.export(User.all, force_quotes: true)
254
+
255
+ runner.import(csv_data)
256
+ ```
257
+
258
+ ## その他
259
+
260
+ ### 本フレームワークに定義されたModel一覧
261
+
262
+ For CSV
263
+
264
+ * base
265
+ * has_one な関連モデル用。 has_many な関連モデルには使えない
266
+ * indexed
267
+ * has_many な関連モデル用。 has_one な関連モデルには使えない
268
+ * `size` オプションでインポート/エクスポートするサイズを指定
269
+ * 属性の translation は `投稿%{index}のタイトル` のように指定
270
+ * モデルの translation は `ユーザ%{index}の` のように指定(関連モデルがさらに indexed だった場合に使われます)
271
+
272
+ ### 本フレームワークに定義されたAttribute一覧
273
+
274
+ * base
275
+ * エクスポートでは `to_s` し、インポートでは `presence` する
276
+ * boolean
277
+ * `true` / `false` オプションに、それぞれに対応する文字列を指定。デフォルトは、 `"true"` / `"false"`
278
+ * インポート時、合致しない値は `nil` になる
279
+ * date
280
+ * `format` オプションで、 `Date::DATE_FORMATS` のキーを指定。デフォルトは、 `default`
281
+ * インポート時、合致しない値はエラーになる
282
+ * time
283
+ * `format` オプションで、 `Time::DATE_FORMATS` のキーを指定。デフォルトは、 `default`
284
+ * インポート時、合致しない値はエラーになる
285
+ * booletania
286
+ * 詳細は、 https://github.com/ryoff/booletania
287
+ * インポート時、合致しない値は `nil` になる
288
+ * enumerize
289
+ * 詳細は、 https://github.com/brainspec/enumerize
290
+ * インポート時、合致しない値はエラーになる
291
+ * override
292
+ * モデルに処理を委譲する
293
+ * モデルに `override_import_属性名` / `override_export_属性名` を定義すること
294
+ * インポートでは、インポートデータ、エクスポートでは、 ActiveRecord の属性値が引数となる
295
+ * インポート/エクスポートする値を返すこと
296
+
data/README.md ADDED
@@ -0,0 +1,26 @@
1
+ [Japanese](https://github.com/aki2o/activerecord-bixformer/blob/master/README-ja.md)
2
+
3
+ # What's this?
4
+
5
+ Now on write...
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'activerecord-bixformer'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install activerecord-bixformer
22
+
23
+ ## Usage
24
+
25
+ Now on write...
26
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'activerecord-bixformer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "activerecord-bixformer"
8
+ spec.version = ActiveRecord::Bixformer::VERSION
9
+ spec.authors = ["Hiroaki Otsu"]
10
+ spec.email = ["ootsuhiroaki@gmail.com"]
11
+
12
+ spec.summary = %q{a framework for xross transformer between ActiveRecord and other format.}
13
+ spec.description = %q{a framework for xross transformer between ActiveRecord and other format.}
14
+ spec.homepage = "https://github.com/aki2o/activerecord-bixformer"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency("activerecord", ">= 2.3.0")
22
+
23
+ spec.add_development_dependency('sqlite3', '~> 1.3')
24
+ spec.add_development_dependency('i18n', '~> 0.7.0')
25
+ spec.add_development_dependency('enumerize', '~> 2.0.0')
26
+ spec.add_development_dependency('booletania', '~> 0.0.2')
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.12"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec", "~> 3.0"
31
+ spec.add_development_dependency "database_rewinder", "~> 0.6.4"
32
+ spec.add_development_dependency "stackprof", "~> 0.2.9"
33
+ spec.add_development_dependency "pry"
34
+ spec.add_development_dependency "pry-doc"
35
+ spec.add_development_dependency "pry-byebug"
36
+ spec.add_development_dependency "pry-stack_explorer"
37
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Base
5
+ attr_reader :name, :model, :options
6
+
7
+ def initialize(model, attribute_name, options)
8
+ @model = model
9
+ @name = attribute_name
10
+ @options = options
11
+ end
12
+
13
+ def make_export_value(active_record_value)
14
+ active_record_value.to_s
15
+ end
16
+
17
+ def make_import_value(value)
18
+ value.presence
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,26 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Boolean < ::ActiveRecord::Bixformer::Attribute::Base
5
+ def make_export_value(active_record_value)
6
+ true_value = (@options.is_a?(::Hash) && @options[:true]) || 'true'
7
+ false_value = (@options.is_a?(::Hash) && @options[:false]) || 'false'
8
+
9
+ active_record_value.present? ? true_value : false_value
10
+ end
11
+
12
+ def make_import_value(value)
13
+ true_value = (@options.is_a?(::Hash) && @options[:true]) || 'true'
14
+ false_value = (@options.is_a?(::Hash) && @options[:false]) || 'false'
15
+
16
+ case value
17
+ when true_value
18
+ true
19
+ when false_value
20
+ false
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Booletania < ::ActiveRecord::Bixformer::Attribute::Base
5
+ def make_export_value(active_record_value)
6
+ return nil unless @model.data_source
7
+
8
+ @model.data_source.__send__("#{@name}_text")
9
+ end
10
+
11
+ def make_import_value(value)
12
+ return nil unless value
13
+
14
+ @model.activerecord_constant.__send__("#{@name}_options").find do |options|
15
+ options.first == value.strip
16
+ end&.last
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Date < ::ActiveRecord::Bixformer::Attribute::Base
5
+ def make_export_value(active_record_value)
6
+ return nil unless active_record_value
7
+
8
+ active_record_value.to_s(option_format)
9
+ end
10
+
11
+ def make_import_value(value)
12
+ return nil if value.blank?
13
+
14
+ begin
15
+ ::Date.parse(value)
16
+ rescue => e
17
+ format_string = ::Date::DATE_FORMATS[option_format]
18
+
19
+ if format_string
20
+ ::Date.strptime(value, format_string)
21
+ else
22
+ raise e
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def option_format
30
+ (@options.is_a?(::Hash) && @options[:format]) || :default
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Enumerize < ::ActiveRecord::Bixformer::Attribute::Base
5
+ def make_export_value(active_record_value)
6
+ return nil unless @model.data_source
7
+
8
+ @model.data_source.__send__("#{@name}_text")
9
+ end
10
+
11
+ def make_import_value(value)
12
+ return nil if value.blank?
13
+
14
+ @model.activerecord_constant.__send__(@name).options.to_h[value.strip] or
15
+ raise ArgumentError.new "Not acceptable enumerize value : #{value}"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Override < ::ActiveRecord::Bixformer::Attribute::Base
5
+ def make_export_value(active_record_value)
6
+ @model.__send__("override_export_#{@name}", active_record_value)
7
+ end
8
+
9
+ def make_import_value(value)
10
+ @model.__send__("override_import_#{@name}", value)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Attribute
4
+ class Time < ::ActiveRecord::Bixformer::Attribute::Base
5
+ def make_export_value(active_record_value)
6
+ return nil unless active_record_value
7
+
8
+ active_record_value.to_s(option_format)
9
+ end
10
+
11
+ def make_import_value(value)
12
+ return nil if value.blank?
13
+
14
+ begin
15
+ ::Time.parse(value)
16
+ rescue => e
17
+ format_string = ::Time::DATE_FORMATS[option_format]
18
+
19
+ if format_string
20
+ ::Time.strptime(value, format_string)
21
+ else
22
+ raise e
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def option_format
30
+ (@options.is_a?(::Hash) && @options[:format]) || :default
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end