yaml_exporter 0.1.0 → 0.2.1
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 +4 -4
- data/.github/workflows/gem-push.yml +20 -29
- data/.github/workflows/test.yml +27 -0
- data/AGENTS.md +34 -0
- data/CHANGELOG.md +56 -0
- data/Gemfile.lock +106 -0
- data/README.md +785 -37
- data/RELEASING.md +45 -0
- data/Rakefile +14 -0
- data/lib/yaml_exporter/builder.rb +103 -0
- data/lib/yaml_exporter/exporter.rb +139 -0
- data/lib/yaml_exporter/importer.rb +86 -0
- data/lib/yaml_exporter/nodes/attribute.rb +70 -0
- data/lib/yaml_exporter/nodes/many_base.rb +215 -0
- data/lib/yaml_exporter/nodes/many_find_by.rb +70 -0
- data/lib/yaml_exporter/nodes/many_positional.rb +14 -0
- data/lib/yaml_exporter/nodes/many_reference.rb +99 -0
- data/lib/yaml_exporter/nodes/many_through.rb +195 -0
- data/lib/yaml_exporter/nodes/of_resolution.rb +84 -0
- data/lib/yaml_exporter/nodes/one_owned.rb +96 -0
- data/lib/yaml_exporter/nodes/one_reference.rb +72 -0
- data/lib/yaml_exporter/nodes/one_reference_of.rb +85 -0
- data/lib/yaml_exporter/schema.rb +24 -0
- data/lib/yaml_exporter/structure.rb +25 -0
- data/lib/yaml_exporter/type_inference.rb +64 -0
- data/lib/yaml_exporter/version.rb +5 -0
- data/lib/yaml_exporter.rb +54 -8
- data/script/demo_quiz.rb +78 -0
- data/yaml_exporter.gemspec +8 -9
- metadata +59 -13
- data/lib/yaml_serializable/structure_builder.rb +0 -30
- data/lib/yaml_serializable/version.rb +0 -3
- data/lib/yaml_serializable/yaml_exporter.rb +0 -196
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
require 'yaml'
|
|
2
|
-
require 'json-schema'
|
|
3
|
-
|
|
4
|
-
module YamlSerializable
|
|
5
|
-
class YamlExporter
|
|
6
|
-
def self.export(object, structure)
|
|
7
|
-
new(object, structure).export
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def self.import(object, yaml_string, structure)
|
|
11
|
-
new(object, structure).import(yaml_string)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def self.generate_schema(klass, structure)
|
|
15
|
-
new(klass.new, structure).generate_schema
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def initialize(object, structure)
|
|
19
|
-
@object = object
|
|
20
|
-
@structure = structure
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def export
|
|
24
|
-
yaml = YAML.dump(build_hash(@structure, @object))
|
|
25
|
-
validate(yaml)
|
|
26
|
-
yaml
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def import(yaml_string)
|
|
30
|
-
data = YAML.safe_load(yaml_string)
|
|
31
|
-
validate(yaml_string)
|
|
32
|
-
update_object(data)
|
|
33
|
-
@object
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def generate_schema
|
|
37
|
-
{
|
|
38
|
-
type: 'object',
|
|
39
|
-
properties: generate_properties(@structure, @object.class),
|
|
40
|
-
required: required_attributes(@structure, @object.class)
|
|
41
|
-
}
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def build_hash(structure, object)
|
|
47
|
-
result = {}
|
|
48
|
-
|
|
49
|
-
structure[:attributes].each do |attr|
|
|
50
|
-
value = object.send(attr)
|
|
51
|
-
result[attr.to_s] = value unless value.nil?
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
structure[:associations].each do |name, config|
|
|
55
|
-
if config[:type] == :has_many
|
|
56
|
-
associated_objects = object.send(name).unscope(:order).order(:id)
|
|
57
|
-
if associated_objects.any?
|
|
58
|
-
result[name.to_s] = associated_objects.map { |item| build_hash(config[:structure], item) }
|
|
59
|
-
end
|
|
60
|
-
elsif config[:type] == :has_one
|
|
61
|
-
associated_object = object.send(name)
|
|
62
|
-
result[name.to_s] = build_hash(config[:structure], associated_object) if associated_object
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
result.compact
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def validate(yaml)
|
|
70
|
-
schema = generate_schema
|
|
71
|
-
data = YAML.safe_load(yaml)
|
|
72
|
-
JSON::Validator.validate!(schema, data)
|
|
73
|
-
rescue JSON::Schema::ValidationError => e
|
|
74
|
-
raise "Invalid YAML structure: #{e.message}"
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def update_object(data)
|
|
78
|
-
@object.transaction do
|
|
79
|
-
update_attributes(@object, data, @structure[:attributes])
|
|
80
|
-
@structure[:associations].each do |name, config|
|
|
81
|
-
if config[:type] == :has_many
|
|
82
|
-
update_collection(@object, data[name.to_s], name, config[:structure])
|
|
83
|
-
elsif config[:type] == :has_one && data[name.to_s]
|
|
84
|
-
update_nested(@object, data[name.to_s], name, config[:structure])
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
@object.save!
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def update_attributes(object, data, attributes)
|
|
92
|
-
attributes.each do |attr|
|
|
93
|
-
if data.key?(attr.to_s)
|
|
94
|
-
object.send("#{attr}=", data[attr.to_s])
|
|
95
|
-
else
|
|
96
|
-
object.send("#{attr}=", nil)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def update_collection(parent, items_data, association_name, config)
|
|
102
|
-
existing_items = parent.send(association_name).unscope(:order).order(:id).to_a
|
|
103
|
-
new_items = []
|
|
104
|
-
|
|
105
|
-
items_data&.each_with_index do |item_data, index|
|
|
106
|
-
item = existing_items[index] || parent.send(association_name).build
|
|
107
|
-
update_attributes(item, item_data, config[:attributes])
|
|
108
|
-
config[:associations]&.each do |sub_name, sub_config|
|
|
109
|
-
if sub_config[:type] == :has_many
|
|
110
|
-
update_collection(item, item_data[sub_name.to_s], sub_name, sub_config[:structure])
|
|
111
|
-
elsif sub_config[:type] == :has_one && item_data[sub_name.to_s]
|
|
112
|
-
update_nested(item, item_data[sub_name.to_s], sub_name, sub_config[:structure])
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
new_items << item
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Entferne Items, die nicht mehr im YAML vorhanden sind
|
|
119
|
-
(existing_items - new_items).each(&:mark_for_destruction)
|
|
120
|
-
|
|
121
|
-
parent.send("#{association_name}=", new_items)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def update_nested(parent, nested_data, association_name, config)
|
|
125
|
-
nested_object = parent.send(association_name) || parent.send("build_#{association_name}")
|
|
126
|
-
update_attributes(nested_object, nested_data, config[:attributes])
|
|
127
|
-
config[:associations]&.each do |sub_name, sub_config|
|
|
128
|
-
if sub_config[:type] == :has_many
|
|
129
|
-
update_collection(nested_object, nested_data[sub_name.to_s], sub_name, sub_config[:structure])
|
|
130
|
-
elsif sub_config[:type] == :has_one && nested_data[sub_name.to_s]
|
|
131
|
-
update_nested(nested_object, nested_data[sub_name.to_s], sub_name, sub_config[:structure])
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def generate_properties(structure, klass)
|
|
137
|
-
properties = {}
|
|
138
|
-
|
|
139
|
-
structure[:attributes].each do |attr|
|
|
140
|
-
column = klass.columns_hash[attr.to_s]
|
|
141
|
-
properties[attr.to_s] = { type: infer_type(column) }
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
structure[:associations].each do |name, config|
|
|
145
|
-
if config[:type] == :has_many
|
|
146
|
-
properties[name.to_s] = {
|
|
147
|
-
type: 'array',
|
|
148
|
-
items: {
|
|
149
|
-
type: 'object',
|
|
150
|
-
properties: generate_properties(config[:structure], association_class(klass, name)),
|
|
151
|
-
required: required_attributes(config[:structure], association_class(klass, name))
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
elsif config[:type] == :has_one
|
|
155
|
-
properties[name.to_s] = {
|
|
156
|
-
type: 'object',
|
|
157
|
-
properties: generate_properties(config[:structure], association_class(klass, name)),
|
|
158
|
-
required: required_attributes(config[:structure], association_class(klass, name))
|
|
159
|
-
}
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
properties
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def infer_type(column)
|
|
167
|
-
case column.type
|
|
168
|
-
when :string, :text
|
|
169
|
-
'string'
|
|
170
|
-
when :integer, :bigint
|
|
171
|
-
'integer'
|
|
172
|
-
when :float, :decimal
|
|
173
|
-
'number'
|
|
174
|
-
when :boolean
|
|
175
|
-
'boolean'
|
|
176
|
-
when :date, :datetime, :time
|
|
177
|
-
'string'
|
|
178
|
-
when :json, :jsonb
|
|
179
|
-
'object'
|
|
180
|
-
else
|
|
181
|
-
'string'
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def association_class(klass, association_name)
|
|
186
|
-
klass.reflect_on_association(association_name).klass
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def required_attributes(structure, klass)
|
|
190
|
-
structure[:attributes].select do |attr|
|
|
191
|
-
column = klass.columns_hash[attr.to_s]
|
|
192
|
-
column && !column.null
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
end
|
|
196
|
-
end
|