yaml_exporter 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
+ SHA256:
3
+ metadata.gz: 11669d9671c43a7dced493565d65af864c3e0b61ba583c40b678f547dc952796
4
+ data.tar.gz: 2f235b07235119bffc293e3030bbeaedfb8f4eca263ef921d0076a83bf658e0c
5
+ SHA512:
6
+ metadata.gz: 29676b99c64faa28c7e70fddd67fb1376fe9d0ff1bf1657318197367021313164048eb8e75d8ff7e6cbb90b0eba48bbc71ee7aa3fe8ab32bc23e431adc6a8d0b
7
+ data.tar.gz: 618c63aa98999137b72d3dfc030cb9d861f29a60c333671d58fa21e71974ced9041dd10a6d2c49176669c627d9edca92a19965743945682225e807bffe8f41ef
@@ -0,0 +1,36 @@
1
+ name: Ruby Gem
2
+
3
+ on:
4
+ push:
5
+ branches: [ "main" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ build:
11
+ name: Build + Publish
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+ packages: write
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - name: Set up Ruby 3.3.x
20
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
21
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
22
+ # uses: ruby/setup-ruby@v1
23
+ uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: 3.3.4
26
+
27
+ - name: Publish to RubyGems
28
+ run: |
29
+ mkdir -p $HOME/.gem
30
+ touch $HOME/.gem/credentials
31
+ chmod 0600 $HOME/.gem/credentials
32
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
33
+ gem build *.gemspec
34
+ gem push *.gem
35
+ env:
36
+ GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}"
data/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Anatoly Zelenin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # YamlExporter
2
+
3
+ YamlExporter is a Ruby gem that provides YAML serialization and deserialization functionality for ActiveRecord models,
4
+ with JSON schema generation for validation.
5
+
6
+ ## Features
7
+
8
+ - Serialize ActiveRecord models to YAML
9
+ - Deserialize YAML back to ActiveRecord models
10
+ - Generate JSON schemas for model validation
11
+ - Support for nested associations (has_many and has_one)
12
+ - Automatic type inference based on database column types
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'yaml_exporter'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```
31
+ $ gem install yaml_exporter
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Setting up your model
37
+
38
+ Include the `YamlExporter` module in your ActiveRecord model and define the YAML structure:
39
+
40
+ ```ruby
41
+
42
+ class Quiz < ApplicationRecord
43
+ include YamlExporter
44
+
45
+ yaml_structure do
46
+ yaml_attribute :title, :quiz_type
47
+ yaml_has_many :questions do
48
+ yaml_attribute :text, :question_type, :feedback
49
+ yaml_has_many :answers do
50
+ yaml_attribute :text, :is_correct, :impact
51
+ end
52
+ end
53
+ end
54
+ end
55
+ ```
56
+
57
+ ### Serializing to YAML
58
+
59
+ To serialize a model instance to YAML:
60
+
61
+ ```ruby
62
+ quiz = Quiz.find(1)
63
+ yaml_string = quiz.yaml_export
64
+ ```
65
+
66
+ ### Deserializing from YAML
67
+
68
+ To deserialize YAML back to a model instance:
69
+
70
+ ```ruby
71
+ quiz = Quiz.new
72
+ quiz.yaml_import(yaml_string)
73
+ ```
74
+
75
+ ### Generating JSON Schema
76
+
77
+ To generate a JSON schema for validation:
78
+
79
+ ```ruby
80
+ schema = Quiz.yaml_schema
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ The YamlExporter automatically infers types based on the database column types. JSON and JSONB columns are treated as
86
+ objects in the generated schema.
87
+
88
+ ## Contributing
89
+
90
+ Bug reports and pull requests are welcome on GitHub at https://github.com/itadventurer/yaml_exporter.
91
+
92
+ ## License
93
+
94
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'yaml_serializable/structure_builder'
4
+ require_relative 'yaml_serializable/yaml_exporter'
5
+
6
+ module YamlExporter
7
+ def self.included(base)
8
+ base.extend(ClassMethods)
9
+ end
10
+
11
+ module ClassMethods
12
+ def yaml_structure(&block)
13
+ class_attribute :yaml_structure_definition, instance_writer: false
14
+ self.yaml_structure_definition = YamlSerializable::StructureBuilder.new(&block).build
15
+ include InstanceMethods
16
+ end
17
+
18
+ def yaml_schema
19
+ YamlSerializable::YamlExporter.generate_schema(self, yaml_structure_definition)
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ def yaml_export
25
+ YamlSerializable::YamlExporter.export(self, self.class.yaml_structure_definition)
26
+ end
27
+
28
+ def yaml_import(yaml_string)
29
+ YamlSerializable::YamlExporter.import(self, yaml_string, self.class.yaml_structure_definition)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlSerializable
4
+ class StructureBuilder
5
+ def initialize(&block)
6
+ @structure = { attributes: [], associations: {} }
7
+ instance_eval(&block)
8
+ end
9
+
10
+ def yaml_attribute(*attrs)
11
+ @structure[:attributes].concat(attrs)
12
+ end
13
+
14
+ def yaml_has_one(name, &block)
15
+ @structure[:associations][name] = { type: :has_one, structure: self.class.new(&block).build }
16
+ end
17
+
18
+ def yaml_has_many(name, &block)
19
+ @structure[:associations][name] = { type: :has_many, structure: self.class.new(&block).build }
20
+ end
21
+
22
+ def yaml_condition(&block)
23
+ @structure[:condition] = block
24
+ end
25
+
26
+ def build
27
+ @structure
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ module YamlSerializable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,196 @@
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
@@ -0,0 +1,31 @@
1
+ require_relative "lib/yaml_serializable/version"
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "yaml_exporter"
5
+ spec.version = YamlSerializable::VERSION
6
+ spec.authors = ["Anatoly Zelenin"]
7
+ spec.email = ["anatoly@zelenin.de"]
8
+
9
+ spec.summary = "YAML serialization for ActiveRecord models with JSON schema support"
10
+ spec.description = "A Ruby gem for YAML serialization and deserialization of ActiveRecord models with JSON schema generation."
11
+ spec.homepage = "https://github.com/itadventurer/yaml_exporter"
12
+ spec.license = "MIT"
13
+
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+ spec.metadata["source_code_uri"] = "https://github.com/itadventurer/yaml_exporter"
18
+ #spec.metadata["changelog_uri"] = "https://github.com/itadventurer/yaml_exporter/blob/master/CHANGELOG.md"
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
+ end
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "activerecord", ">= 5.2"
28
+ spec.add_dependency "activesupport", ">= 5.2"
29
+
30
+ spec.add_development_dependency "rake", "~> 13.0"
31
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yaml_exporter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Anatoly Zelenin
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '5.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '5.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ description: A Ruby gem for YAML serialization and deserialization of ActiveRecord
56
+ models with JSON schema generation.
57
+ email:
58
+ - anatoly@zelenin.de
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".github/workflows/gem-push.yml"
64
+ - ".gitignore"
65
+ - Gemfile
66
+ - LICENSE
67
+ - README.md
68
+ - lib/yaml_exporter.rb
69
+ - lib/yaml_serializable/structure_builder.rb
70
+ - lib/yaml_serializable/version.rb
71
+ - lib/yaml_serializable/yaml_exporter.rb
72
+ - yaml_exporter.gemspec
73
+ homepage: https://github.com/itadventurer/yaml_exporter
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ homepage_uri: https://github.com/itadventurer/yaml_exporter
78
+ source_code_uri: https://github.com/itadventurer/yaml_exporter
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 2.5.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.5.11
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: YAML serialization for ActiveRecord models with JSON schema support
98
+ test_files: []