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.
@@ -0,0 +1,170 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Generator
4
+ class ActiveRecord < ::ActiveRecord::Bixformer::Generator::Base
5
+ private
6
+
7
+ def association_generator
8
+ :new_as_association_for_import
9
+ end
10
+
11
+ def generate_model_value(model)
12
+ generate_attributes_value(model).merge(generate_association_value(model))
13
+ end
14
+
15
+ def generate_attributes_value(model)
16
+ attribute_value_map = model.generate_import_value_map
17
+ required_attributes = @modeler.config_value_for(model, :required_attributes, [])
18
+ identified_column_name = identified_column_name_of(model)
19
+
20
+ # 必須な属性が渡されていない場合には、取り込みしない
21
+ return {} if required_attributes.any? { |attribute_name| ! presence_value?(attribute_value_map[attribute_name]) }
22
+
23
+ set_required_condition(model, attribute_value_map)
24
+ set_parent_key(model, attribute_value_map, identified_column_name)
25
+ set_activerecord_id(model, attribute_value_map, identified_column_name)
26
+
27
+ # 空でない要素が無いなら、空ハッシュで返す
28
+ presence_value?(attribute_value_map) ? attribute_value_map : {}
29
+ end
30
+
31
+ def generate_association_value(parent_model)
32
+ association_value_map = {}.with_indifferent_access
33
+
34
+ parent_model.association_map.each do |association_name, model_or_models|
35
+ association_value = if model_or_models.is_a?(::Array)
36
+ model_or_models.map { |m| generate_model_value(m) }.reject { |v| ! presence_value?(v) }
37
+ else
38
+ generate_model_value(model_or_models)
39
+ end
40
+
41
+ # 取り込み時は、オプショナルな関連では、空と思われる値は取り込まない
42
+ next if ! presence_value?(association_value) &&
43
+ parent_model.optional_attributes.include?(association_name.to_s)
44
+
45
+ association_value_map["#{association_name}_attributes".to_sym] = association_value
46
+ end
47
+
48
+ association_value_map
49
+ end
50
+
51
+ def set_required_condition(model, attribute_value_map)
52
+ # 結果ハッシュが空なら、取り込みしないように追加はしない
53
+ return unless presence_value?(attribute_value_map)
54
+
55
+ # 設定するのはルートの場合のみ
56
+ return if model.parent
57
+
58
+ attribute_value_map.merge!(@modeler.required_condition)
59
+ end
60
+
61
+ def set_parent_key(model, attribute_value_map, identified_column_name)
62
+ # 結果ハッシュが空なら、取り込みしないように追加はしない
63
+ return unless presence_value?(attribute_value_map)
64
+
65
+ # 設定するのは親がいる場合のみ
66
+ return unless model.parent
67
+
68
+ parent_id = model.parent&.activerecord_id
69
+
70
+ if parent_id
71
+ # 親のレコードが見つかっているなら、それも結果ハッシュに追加する
72
+ attribute_value_map[model.parent_foreign_key] = parent_id
73
+ else
74
+ # 見つかっていないなら、間違った値が指定されている可能性があるので、キー自体を削除
75
+ attribute_value_map.delete(model.parent_foreign_key)
76
+ end
77
+ end
78
+
79
+ def set_activerecord_id(model, attribute_value_map, identified_column_name)
80
+ # 更新の場合は、インポートデータを元にデータベースから対象のレコードを検索してIDを取得
81
+ model.activerecord_id = verified_activerecord_id(model, attribute_value_map, identified_column_name)
82
+
83
+ if model.activerecord_id
84
+ # 更新なら、ID属性を改めて設定
85
+ attribute_value_map[identified_column_name] = model.activerecord_id
86
+ else
87
+ # 見つかっていないなら、間違った値が指定されている可能性があるので、キー自体を削除
88
+ attribute_value_map.delete(identified_column_name)
89
+ end
90
+ end
91
+
92
+ def verified_activerecord_id(model, attribute_value_map, identified_column_name)
93
+ # 更新対象のレコードを特定できるかチェック
94
+ identified_value = attribute_value_map[identified_column_name]
95
+
96
+ uniqueness_condition = if identified_value
97
+ { identified_column_name => identified_value }
98
+ else
99
+ find_unique_condition(model, attribute_value_map, identified_column_name)
100
+ end
101
+
102
+ # レコードが特定できないなら、更新処理ではないので終了
103
+ return nil unless uniqueness_condition
104
+
105
+ # 更新対象のレコードを正しく特定できているか確認するための検証条件を取得
106
+ required_condition = if model.parent
107
+ key = model.parent_foreign_key
108
+
109
+ { key => attribute_value_map[key] }
110
+ else
111
+ @modeler.required_condition
112
+ end
113
+
114
+ # 検証条件は、必ず値がなければならない
115
+ return nil if required_condition.any? { |_k, v| ! presence_value?(v) }
116
+
117
+ # インポートされてきた、レコードを特定する条件が、誤った値でないかどうかを、
118
+ # 特定されるレコードが、更新すべき正しいレコードであるかチェックするための
119
+ # 検証条件とマージして、データベースに登録されているか確認する
120
+ verified_condition = uniqueness_condition.merge(required_condition)
121
+
122
+ find_verified_activerecord_by!(model.activerecord_constant, verified_condition).__send__(identified_column_name)
123
+ rescue ::ActiveRecord::RecordNotFound => e
124
+ # ID属性が指定されているのに、データベースに見つからない場合はエラーにする
125
+ raise e if identified_value
126
+ end
127
+
128
+ def find_unique_condition(model, attribute_value_map, identified_column_name)
129
+ unique_indexes = @modeler.config_value_for(model, :unique_indexes, [])
130
+
131
+ # ユニーク条件が指定されていないなら終了
132
+ return nil if unique_indexes.empty?
133
+
134
+ unique_condition = unique_indexes.map do |key|
135
+ [key, attribute_value_map[key]]
136
+ end.to_h
137
+
138
+ # ユニーク条件は、必ず値がなければならない
139
+ return nil if unique_condition.any? { |_k, v| ! presence_value?(v) }
140
+
141
+ unique_condition
142
+ end
143
+
144
+ def find_verified_activerecord_by!(klass, verified_uniqueness_condition)
145
+ klass.find_by!(verified_uniqueness_condition)
146
+ end
147
+
148
+ def identified_column_name_of(model)
149
+ model.activerecord_constant.primary_key
150
+ end
151
+
152
+ def presence_value?(value)
153
+ # 空でない要素であるか or 空でない要素を含んでいるかどうか
154
+ case value
155
+ when ::Hash
156
+ value.values.any? { |v| presence_value?(v) }
157
+ when ::Array
158
+ value.any? { |v| presence_value?(v) }
159
+ when ::String
160
+ ! value.blank?
161
+ when ::TrueClass, ::FalseClass
162
+ true
163
+ else
164
+ value ? true : false
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,66 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Generator
4
+ class Base
5
+ attr_reader :data_source
6
+
7
+ def initialize(modeler, data_source)
8
+ @modeler = modeler
9
+ @data_source = data_source
10
+ end
11
+
12
+ def compile
13
+ return @model if @model
14
+
15
+ model_name = @modeler.model_name
16
+ model_type, model_options = @modeler.parse_to_type_and_options(@modeler.entry_definition[:type])
17
+
18
+ @model = @modeler.new_module_instance(:model, model_type, model_name, model_options)
19
+
20
+ @model.data_source = @data_source
21
+
22
+ compile_model(@model)
23
+
24
+ @model
25
+ end
26
+
27
+ def generate
28
+ generate_model_value(compile)
29
+ end
30
+
31
+ private
32
+
33
+ def compile_model(model)
34
+ model.setup_with_modeler(@modeler)
35
+
36
+ compile_associations(model)
37
+ end
38
+
39
+ def compile_associations(parent_model)
40
+ association_definitions = @modeler.config_value_for(parent_model, :entry_definition, {})[:associations] || {}
41
+
42
+ association_definitions.each do |association_name, association_definition|
43
+ association_type, association_options = @modeler.parse_to_type_and_options(association_definition[:type])
44
+ association_constant = @modeler.find_module_constant(:model, association_type)
45
+
46
+ model_or_models = association_constant.__send__(
47
+ association_generator, parent_model, association_name, association_options
48
+ )
49
+
50
+ parent_model.add_association(model_or_models)
51
+
52
+ if model_or_models.is_a?(::Array)
53
+ model_or_models.each { |model| compile_model(model) }
54
+ else
55
+ compile_model(model_or_models)
56
+ end
57
+ end
58
+ end
59
+
60
+ def association_generator
61
+ raise ::NotImplementedError.new "You must implement #{self.class}##{__method__}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Generator
4
+ class CsvRow < ::ActiveRecord::Bixformer::Generator::Base
5
+ private
6
+
7
+ def association_generator
8
+ :new_as_association_for_export
9
+ end
10
+
11
+ def generate_model_value(model)
12
+ generate_attributes_value(model).merge(generate_association_value(model))
13
+ end
14
+
15
+ def generate_attributes_value(model)
16
+ model.generate_export_value_map.map do |attribute_name, attribute_value|
17
+ [model.csv_title(attribute_name), attribute_value]
18
+ end.to_h
19
+ end
20
+
21
+ def generate_association_value(parent_model)
22
+ parent_model.association_map.values.inject({}) do |association_value, model_or_models|
23
+ models = model_or_models.is_a?(::Array) ? model_or_models : [model_or_models]
24
+
25
+ # 全関連レコードの生成結果を単一ハッシュにマージ
26
+ association_value.merge(
27
+ models.inject({}) do |current_association_value, model|
28
+ # 関連レコードの全生成結果を単一ハッシュにマージ
29
+ current_association_value.merge(
30
+ # キーをCSVカラム名に置き換えたハッシュを作成
31
+ generate_model_value(model)
32
+ )
33
+ end
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,150 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Model
4
+ class Base
5
+ attr_accessor :data_source,
6
+ :activerecord_id
7
+
8
+ attr_reader :name,
9
+ :parent,
10
+ :attribute_map,
11
+ :optional_attributes,
12
+ :association_map,
13
+ :translator,
14
+ :modeler
15
+
16
+ class << self
17
+ def new_as_association_for_import(parent, association_name, options)
18
+ raise ::NotImplementedError.new "You must implement #{self.class}##{__method__}"
19
+ end
20
+
21
+ def new_as_association_for_export(parent, association_name, options)
22
+ model = self.new(association_name, options)
23
+
24
+ model.data_source = parent.data_source && parent.data_source.__send__(association_name) # parent.data_source is ActiveRecord::Base
25
+
26
+ unless model.data_source.is_a?(::ActiveRecord::Base)
27
+ parent_name = model.parents.map(&:name).join('.')
28
+
29
+ raise ::ArgumentError.new "#{parent_name}.#{association_name} is not a ActiveRecord instance"
30
+ end
31
+
32
+ model
33
+ end
34
+ end
35
+
36
+ def initialize(model_or_association_name, options)
37
+ @name = model_or_association_name.to_s
38
+ @options = options
39
+ @association_map = {}
40
+ end
41
+
42
+ def setup_with_modeler(modeler)
43
+ @modeler = modeler
44
+
45
+ entry_definition = @modeler.config_value_for(self, :entry_definition, {})
46
+
47
+ @attribute_map = (entry_definition[:attributes] || {}).map do |attribute_name, attribute_value|
48
+ attribute_type, attribute_options = @modeler.parse_to_type_and_options(attribute_value)
49
+
50
+ attribute = @modeler.new_module_instance(:attribute, attribute_type, self, attribute_name, attribute_options)
51
+
52
+ [attribute_name, attribute]
53
+ end.to_h
54
+
55
+ @optional_attributes = @modeler.config_value_for(self, :optional_attributes, [])
56
+ @default_values = @modeler.config_value_for(self, :default_values, {})
57
+
58
+ # At present, translation function is only i18n
59
+ @translator = ::ActiveRecord::Bixformer::Translator::I18n.new
60
+
61
+ @translator.config = @modeler.translation_config.dup
62
+ @translator.model = self
63
+ end
64
+
65
+ def set_parent(model)
66
+ @parent = model
67
+ end
68
+
69
+ def parents
70
+ @parent ? [*parent.parents, @parent] : []
71
+ end
72
+
73
+ def parent_foreign_key
74
+ return nil unless @parent
75
+
76
+ @parent_foreign_key ||= @parent.activerecord_constant.reflections[@name].foreign_key
77
+ end
78
+
79
+ def add_association(model_or_models)
80
+ models = model_or_models.is_a?(::Array) ? model_or_models : [model_or_models]
81
+
82
+ association_name = models.first.name
83
+
84
+ @association_map[association_name] = model_or_models
85
+
86
+ models.each { |model| model.set_parent(self) }
87
+ end
88
+
89
+ def activerecord_constant
90
+ @activerecord_constant ||=
91
+ if @parent
92
+ @parent.activerecord_constant.reflections[@name].table_name.classify.constantize
93
+ else
94
+ @name.camelize.constantize
95
+ end
96
+ end
97
+
98
+ def generate_export_value_map
99
+ @attribute_map.keys.map do |attribute_name|
100
+ [attribute_name, make_export_value(attribute_name)]
101
+ end.to_h.with_indifferent_access
102
+ end
103
+
104
+ def generate_import_value_map
105
+ value_map = {}.with_indifferent_access
106
+
107
+ @attribute_map.keys.each do |attribute_name|
108
+ attribute_value = make_import_value(attribute_name)
109
+
110
+ attribute_value = @default_values[attribute_name] unless presence_value?(attribute_value)
111
+
112
+ # 取り込み時は、オプショナルな属性では、空と思われる値は取り込まない
113
+ next if ! presence_value?(attribute_value) &&
114
+ @optional_attributes.include?(attribute_name.to_s)
115
+
116
+ value_map[attribute_name] = attribute_value
117
+ end
118
+
119
+ value_map
120
+ end
121
+
122
+ private
123
+
124
+ def make_export_value(attribute_name)
125
+ return nil unless @data_source
126
+
127
+ attribute = @attribute_map[attribute_name]
128
+ data_source_attribute_value = @data_source.__send__(attribute_name)
129
+
130
+ attribute.make_export_value(data_source_attribute_value)
131
+ end
132
+
133
+ def make_import_value(attribute_name)
134
+ raise ::NotImplementedError.new "You must implement #{self.class}##{__method__}"
135
+ end
136
+
137
+ def presence_value?(value)
138
+ case value
139
+ when ::String
140
+ ! value.blank?
141
+ when ::TrueClass, ::FalseClass
142
+ true
143
+ else
144
+ value ? true : false
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Model
4
+ module Csv
5
+ class Base < ::ActiveRecord::Bixformer::Model::Base
6
+ class << self
7
+ def new_as_association_for_import(parent, association_name, options)
8
+ model = self.new(association_name, options)
9
+
10
+ model.data_source = parent.data_source # parent.data_source is CSV::Row
11
+
12
+ model
13
+ end
14
+ end
15
+
16
+ def csv_title(attribute_name)
17
+ @translator.translate_attribute(attribute_name)
18
+ end
19
+
20
+ def available_csv_titles
21
+ [
22
+ *@attribute_map.keys.map do |attribute_name|
23
+ csv_title(attribute_name)
24
+ end,
25
+ *@association_map.values.flat_map do |model_or_models|
26
+ models = model_or_models.is_a?(::Array) ? model_or_models : [model_or_models]
27
+
28
+ models.flat_map { |m| m.available_csv_titles }
29
+ end
30
+ ]
31
+ end
32
+
33
+ private
34
+
35
+ def make_import_value(attribute_name)
36
+ return nil unless @data_source
37
+
38
+ attribute = @attribute_map[attribute_name]
39
+ data_source_attribute_value = @data_source[csv_title(attribute_name)]
40
+
41
+ attribute.make_import_value(data_source_attribute_value)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Model
4
+ module Csv
5
+ class Indexed < ::ActiveRecord::Bixformer::Model::Csv::Base
6
+ class << self
7
+ def new_as_association_for_import(parent, association_name, options)
8
+ options = options.is_a?(Hash) ? options : {}
9
+ limit_size = options[:size] || 1
10
+
11
+ (1..limit_size).map do |index|
12
+ model = self.new(association_name, options.merge(index: index))
13
+
14
+ model.data_source = parent.data_source # parent.data_source is CSV::Row
15
+
16
+ model
17
+ end
18
+ end
19
+
20
+ def new_as_association_for_export(parent, association_name, options)
21
+ options = options.is_a?(Hash) ? options : {}
22
+ limit_size = options[:size] || 1
23
+ associations = parent.data_source ? parent.data_source.__send__(association_name).to_a : []
24
+
25
+ (1..limit_size).map do |index|
26
+ model = self.new(association_name, options.merge(index: index))
27
+
28
+ model.data_source = associations[index - 1]
29
+
30
+ model
31
+ end
32
+ end
33
+ end
34
+
35
+ def setup_with_modeler(modeler)
36
+ super
37
+
38
+ @translator.model_arguments = { index: @options[:index] }
39
+
40
+ @translator.attribute_arguments_map = @attribute_map.keys.map do |attribute_name|
41
+ [attribute_name, { index: @options[:index] }]
42
+ end.to_h
43
+ end
44
+
45
+ def csv_title(attribute_name)
46
+ if parents.find { |parent| parent.is_a?(ActiveRecord::Bixformer::Model::Csv::Indexed) }
47
+ parents.map { |parent| parent.translator.translate_model }.join + super
48
+ else
49
+ super
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,160 @@
1
+ module ActiveRecord
2
+ module Bixformer
3
+ module Modeler
4
+ class Base
5
+ attr_reader :format
6
+
7
+ def initialize(format)
8
+ @format = format
9
+ @module_load_namespaces_of = {}
10
+ @module_constant_of = {}
11
+ end
12
+
13
+ def model_name
14
+ end
15
+
16
+ def entry_definition
17
+ {}
18
+ end
19
+
20
+ def optional_attributes
21
+ []
22
+ end
23
+
24
+ def required_attributes
25
+ []
26
+ end
27
+
28
+ def unique_indexes
29
+ []
30
+ end
31
+
32
+ def required_condition
33
+ {}
34
+ end
35
+
36
+ def default_values
37
+ {}
38
+ end
39
+
40
+ def translation_config
41
+ {
42
+ scope: :bixformer,
43
+ extend_scopes: []
44
+ }
45
+ end
46
+
47
+ def module_load_namespaces(module_type)
48
+ [
49
+ "::ActiveRecord::Bixformer::#{module_type.to_s.camelize}::#{format.to_s.camelize}",
50
+ "::ActiveRecord::Bixformer::#{module_type.to_s.camelize}",
51
+ ]
52
+ end
53
+
54
+ def config_value_for(model, config_name, default_value = nil)
55
+ model_names_without_root = (model.parents.map(&:name) + [model.name]).drop(1)
56
+
57
+ # 指定された設定の全設定値を取得
58
+ entire_config_value = __send__(config_name)
59
+
60
+ if entire_config_value.is_a?(::Hash)
61
+ # Hashなら、with_indifferent_accessしておく
62
+ entire_config_value = entire_config_value.with_indifferent_access
63
+ elsif entire_config_value.is_a?(::Array) && entire_config_value.last.is_a?(::Hash)
64
+ # Arrayで最後の要素がHashなら、with_indifferent_accessしておく
65
+ config_value = entire_config_value.pop
66
+
67
+ entire_config_value.push config_value.with_indifferent_access
68
+ end
69
+
70
+ # その中から、指定のmodelに対応する設定部分を抽出
71
+ config_value = if config_name == :entry_definition
72
+ find_entry_definition(entire_config_value, model_names_without_root)
73
+ else
74
+ find_nested_config_value(entire_config_value, model_names_without_root)
75
+ end
76
+
77
+ if config_value.is_a?(::Array)
78
+ # Arrayで最後の要素がHashの場合、それは子要素の設定値なので、結果に含めない
79
+ config_value.pop if config_value.last.is_a?(::Hash)
80
+
81
+ # Arrayなら、要素は文字列化しておく
82
+ config_value = config_value.map { |v| v.to_s }
83
+ end
84
+
85
+ config_value || default_value
86
+ end
87
+
88
+ def new_module_instance(module_type, name_or_instance, *initializers)
89
+ name_or_instance = :base unless name_or_instance
90
+
91
+ name_or_instance = name_or_instance.to_s if name_or_instance.is_a?(::Symbol)
92
+
93
+ return name_or_instance unless name_or_instance.is_a?(::String)
94
+
95
+ if initializers.size > 0
96
+ find_module_constant(module_type, name_or_instance).new(*initializers)
97
+ else
98
+ find_module_constant(module_type, name_or_instance).new
99
+ end
100
+ end
101
+
102
+ def find_module_constant(module_type, name)
103
+ name = :base unless name
104
+
105
+ module_constant = @module_constant_of["#{module_type}/#{name}"]
106
+
107
+ return module_constant if module_constant
108
+
109
+ namespaces = @module_load_namespaces_of[module_type] ||= module_load_namespaces(module_type)
110
+
111
+ namespaces.each do |namespace|
112
+ constant = "#{namespace}::#{name.to_s.camelize}".safe_constantize
113
+
114
+ return @module_constant_of["#{module_type}/#{name}"] = constant if constant
115
+ end
116
+
117
+ raise ::ArgumentError.new "Not found module named #{name.to_s.camelize} in module_load_namespaces('#{module_type}')"
118
+ end
119
+
120
+ def parse_to_type_and_options(value)
121
+ value = value.dup if value.is_a?(::Array) || value.is_a?(::Hash)
122
+ type = value.is_a?(::Array) ? value.shift : value
123
+
124
+ arguments = if value.is_a?(::Array) && value.size == 1 && value.first.is_a?(::Hash)
125
+ value.first
126
+ elsif value.is_a?(::Array)
127
+ value
128
+ else
129
+ nil
130
+ end
131
+
132
+ [type, arguments]
133
+ end
134
+
135
+ private
136
+
137
+ def find_nested_config_value(config, keys)
138
+ return config ? config.dup : nil if keys.empty?
139
+
140
+ key = keys.shift
141
+
142
+ # config が Array なら、子要素は最後の要素にハッシュで定義してあるはず
143
+ config_map = config.is_a?(::Array) ? config.last : config
144
+
145
+ return nil unless config_map.is_a?(::Hash)
146
+
147
+ find_nested_config_value(config_map[key], keys)
148
+ end
149
+
150
+ def find_entry_definition(config, keys)
151
+ return config ? config.dup : nil if keys.empty?
152
+
153
+ key = keys.shift
154
+
155
+ find_entry_definition(config[:associations][key], keys)
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end