yaml_exporter 0.1.0 → 0.2.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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlExporter
4
+ # Maps an ActiveRecord column type to a JSON-schema primitive.
5
+ #
6
+ # Single source of truth used by every node that emits a `type:` entry
7
+ # in the generated schema (Nodes::Attribute, Nodes::OneReference,
8
+ # Nodes::ManyReference, and the extra_entry_keys path in Nodes::ManyBase).
9
+ #
10
+ # Unknown or un-resolvable columns fall back to 'string' so the schema
11
+ # always validates as JSON-schema-ish.
12
+ module TypeInference
13
+ # AR column type -> JSON-schema primitive. Date/time stay 'string' on
14
+ # purpose: YAML round-trips them as ISO-8601 strings by default, and we
15
+ # don't (yet) emit a `format:` facet.
16
+ COLUMN_TYPE_TO_JSON_SCHEMA = {
17
+ string: 'string',
18
+ text: 'string',
19
+ uuid: 'string',
20
+ binary: 'string',
21
+ integer: 'integer',
22
+ bigint: 'integer',
23
+ float: 'number',
24
+ decimal: 'number',
25
+ boolean: 'boolean',
26
+ date: 'string',
27
+ datetime: 'string',
28
+ time: 'string',
29
+ timestamp: 'string',
30
+ json: 'object',
31
+ jsonb: 'object'
32
+ }.freeze
33
+
34
+ FALLBACK = 'string'
35
+
36
+ # Returns the JSON-schema type string for `column_name` on `klass`.
37
+ # `klass` may be nil (e.g. declaration-time lookup on an anonymous
38
+ # class whose table doesn't exist yet) — we quietly fall back.
39
+ def self.schema_type_for(klass, column_name)
40
+ return FALLBACK unless klass.respond_to?(:columns_hash)
41
+
42
+ column = klass.columns_hash[column_name.to_s]
43
+ return FALLBACK unless column
44
+
45
+ COLUMN_TYPE_TO_JSON_SCHEMA[column.type] || FALLBACK
46
+ rescue ActiveRecord::ActiveRecordError
47
+ FALLBACK
48
+ end
49
+
50
+ # True when `column_name` on `klass` is a `text` column (as opposed to a
51
+ # `string`/varchar). Drives whether string values export as YAML literal
52
+ # block scalars. Unknown/unresolvable columns are treated as non-text.
53
+ def self.text_column?(klass, column_name)
54
+ return false unless klass.respond_to?(:columns_hash)
55
+
56
+ column = klass.columns_hash[column_name.to_s]
57
+ return false unless column
58
+
59
+ column.type == :text
60
+ rescue ActiveRecord::ActiveRecordError
61
+ false
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlExporter
4
+ VERSION = '0.2.0'
5
+ end
data/lib/yaml_exporter.rb CHANGED
@@ -1,9 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'yaml_serializable/structure_builder'
4
- require_relative 'yaml_serializable/yaml_exporter'
3
+ require 'active_record'
4
+ require 'active_support/core_ext/class/attribute'
5
5
 
6
+ require_relative 'yaml_exporter/version'
7
+
8
+ # Top-level entrypoint: defines the `include YamlExporter` hook and the
9
+ # public API methods (`yaml_structure`, `yaml_schema`, `yaml_import`,
10
+ # `yaml_export`). Parsing, import, export and schema generation live in
11
+ # dedicated classes under `lib/yaml_exporter/`.
6
12
  module YamlExporter
13
+ # Base class for every library-specific error. Inherits from
14
+ # ActiveRecord::ActiveRecordError so existing `rescue ActiveRecord::ActiveRecordError`
15
+ # blocks (and the import transaction wrapper) catch it naturally.
16
+ class Error < ActiveRecord::ActiveRecordError; end
17
+
18
+ # Raised when a YAML document carries a key (top-level or inside an entry)
19
+ # that the corresponding `yaml_structure` scope does not declare.
20
+ class UnknownAttributeError < Error; end
21
+
22
+ # Raised when a `find_by`-identified list contains two entries with the same
23
+ # key value — e.g. two `slug: chapter-1` entries under one `book_parts:`.
24
+ class DuplicateKeyError < Error; end
25
+
7
26
  def self.included(base)
8
27
  base.extend(ClassMethods)
9
28
  end
@@ -11,22 +30,48 @@ module YamlExporter
11
30
  module ClassMethods
12
31
  def yaml_structure(&block)
13
32
  class_attribute :yaml_structure_definition, instance_writer: false
14
- self.yaml_structure_definition = YamlSerializable::StructureBuilder.new(&block).build
33
+ self.yaml_structure_definition = YamlExporter::Builder.new(self, &block).build
15
34
  include InstanceMethods
16
35
  end
17
36
 
18
37
  def yaml_schema
19
- YamlSerializable::YamlExporter.generate_schema(self, yaml_structure_definition)
38
+ YamlExporter::Schema.generate(yaml_structure_definition)
20
39
  end
21
40
  end
22
41
 
23
42
  module InstanceMethods
24
- def yaml_export
25
- YamlSerializable::YamlExporter.export(self, self.class.yaml_structure_definition)
43
+ def yaml_import(yaml_string)
44
+ YamlExporter::Importer.new(self, self.class.yaml_structure_definition).import(yaml_string)
26
45
  end
27
46
 
28
- def yaml_import(yaml_string)
29
- YamlSerializable::YamlExporter.import(self, yaml_string, self.class.yaml_structure_definition)
47
+ # `omit_nil:` (default true) drops keys whose value is empty — a nil
48
+ # attribute, a missing `one` reference, an absent owned `one`, or an empty
49
+ # `many` list. Round-trip safe: import treats a missing key, an explicit
50
+ # `null`, and an empty list identically. Pass `omit_nil: false` to keep
51
+ # explicit `null`s in the file (e.g. so optional fields stay discoverable).
52
+ def yaml_export(omit_nil: true)
53
+ YamlExporter::Exporter.new(
54
+ self, self.class.yaml_structure_definition, omit_nil: omit_nil
55
+ ).to_yaml
30
56
  end
31
57
  end
32
58
  end
59
+
60
+ require 'set'
61
+ require 'yaml'
62
+
63
+ require_relative 'yaml_exporter/structure'
64
+ require_relative 'yaml_exporter/type_inference'
65
+ require_relative 'yaml_exporter/nodes/attribute'
66
+ require_relative 'yaml_exporter/nodes/one_owned'
67
+ require_relative 'yaml_exporter/nodes/one_reference'
68
+ require_relative 'yaml_exporter/nodes/one_reference_of'
69
+ require_relative 'yaml_exporter/nodes/many_base'
70
+ require_relative 'yaml_exporter/nodes/many_positional'
71
+ require_relative 'yaml_exporter/nodes/many_find_by'
72
+ require_relative 'yaml_exporter/nodes/many_reference'
73
+ require_relative 'yaml_exporter/nodes/many_through'
74
+ require_relative 'yaml_exporter/builder'
75
+ require_relative 'yaml_exporter/importer'
76
+ require_relative 'yaml_exporter/exporter'
77
+ require_relative 'yaml_exporter/schema'
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Runnable demo sharing the same schema and models as the test suite.
5
+ # From the repository root:
6
+ #
7
+ # ruby script/demo_quiz.rb
8
+ #
9
+ # One-time: gem install sqlite3 && bundle install
10
+
11
+ require 'bundler/setup'
12
+ require 'active_record'
13
+ require 'json'
14
+ require 'pathname'
15
+
16
+ root = Pathname.new(__dir__).join('..').expand_path
17
+ $LOAD_PATH.unshift root.join('lib').to_s
18
+ require 'yaml_exporter'
19
+
20
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
21
+ ActiveRecord::Base.logger = nil
22
+
23
+ load root.join('test/support/schema.rb')
24
+ load root.join('test/support/models.rb')
25
+
26
+ quiz = Quiz.create!(title: 'Ruby Basics', quiz_type: 'multiple_choice')
27
+
28
+ q1 = quiz.questions.create!(
29
+ text: 'What is Ruby?',
30
+ question_type: 'single_choice',
31
+ feedback: 'Ruby is a dynamic, open-source programming language.'
32
+ )
33
+ q1.answers.create!(text: 'A programming language', is_correct: true, impact: 10)
34
+ q1.answers.create!(text: 'A gemstone', is_correct: false, impact: 0)
35
+ q1.answers.create!(text: 'A color', is_correct: false, impact: 0)
36
+
37
+ q2 = quiz.questions.create!(
38
+ text: 'Which keyword defines a method in Ruby?',
39
+ question_type: 'single_choice',
40
+ feedback: "Use 'def' to define methods."
41
+ )
42
+ q2.answers.create!(text: 'def', is_correct: true, impact: 10)
43
+ q2.answers.create!(text: 'func', is_correct: false, impact: 0)
44
+ q2.answers.create!(text: 'fn', is_correct: false, impact: 0)
45
+
46
+ puts '=' * 60
47
+ puts '1) YAML Export'
48
+ puts '=' * 60
49
+ yaml_output = quiz.yaml_export
50
+ puts yaml_output
51
+
52
+ puts '=' * 60
53
+ puts '2) JSON Schema'
54
+ puts '=' * 60
55
+ schema = Quiz.yaml_schema
56
+ puts JSON.pretty_generate(schema)
57
+
58
+ puts "\n" + '=' * 60
59
+ puts '3) YAML Import (round-trip into a new Quiz)'
60
+ puts '=' * 60
61
+
62
+ imported_quiz = Quiz.create!(title: 'placeholder', quiz_type: 'placeholder')
63
+ imported_quiz.yaml_import(yaml_output)
64
+
65
+ puts "Title: #{imported_quiz.title}"
66
+ puts "Type: #{imported_quiz.quiz_type}"
67
+ puts "Questions: #{imported_quiz.questions.count}"
68
+ imported_quiz.questions.each do |q|
69
+ puts " Q: #{q.text}"
70
+ q.answers.each do |a|
71
+ marker = a.is_correct ? '✓' : '✗'
72
+ puts " #{marker} #{a.text} (impact: #{a.impact || 'n/a'})"
73
+ end
74
+ end
75
+
76
+ puts "\n" + '=' * 60
77
+ puts 'Done — all features working!'
78
+ puts '=' * 60
@@ -1,24 +1,21 @@
1
- require_relative "lib/yaml_serializable/version"
1
+ require_relative "lib/yaml_exporter/version"
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "yaml_exporter"
5
- spec.version = YamlSerializable::VERSION
5
+ spec.version = YamlExporter::VERSION
6
6
  spec.authors = ["Anatoly Zelenin"]
7
7
  spec.email = ["anatoly@zelenin.de"]
8
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."
9
+ spec.summary = "YAML import/export for ActiveRecord models with a small declarative DSL"
10
+ spec.description = "A Ruby gem that declaratively maps ActiveRecord models to YAML documents (and back) via a compact `attributes` / `one` / `many` DSL."
11
11
  spec.homepage = "https://github.com/itadventurer/yaml_exporter"
12
12
  spec.license = "MIT"
13
13
 
14
- spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
14
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.2")
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
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
18
 
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
19
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
20
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
24
21
  end
@@ -27,5 +24,7 @@ Gem::Specification.new do |spec|
27
24
  spec.add_dependency "activerecord", ">= 5.2"
28
25
  spec.add_dependency "activesupport", ">= 5.2"
29
26
 
27
+ spec.add_development_dependency "minitest", "~> 5.0"
30
28
  spec.add_development_dependency "rake", "~> 13.0"
31
- end
29
+ spec.add_development_dependency "sqlite3", ">= 1.4"
30
+ end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yaml_exporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anatoly Zelenin
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-08-29 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '5.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: minitest
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '5.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '5.0'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: rake
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -52,8 +65,22 @@ dependencies:
52
65
  - - "~>"
53
66
  - !ruby/object:Gem::Version
54
67
  version: '13.0'
55
- description: A Ruby gem for YAML serialization and deserialization of ActiveRecord
56
- models with JSON schema generation.
68
+ - !ruby/object:Gem::Dependency
69
+ name: sqlite3
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '1.4'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '1.4'
82
+ description: A Ruby gem that declaratively maps ActiveRecord models to YAML documents
83
+ (and back) via a compact `attributes` / `one` / `many` DSL.
57
84
  email:
58
85
  - anatoly@zelenin.de
59
86
  executables: []
@@ -61,14 +88,34 @@ extensions: []
61
88
  extra_rdoc_files: []
62
89
  files:
63
90
  - ".github/workflows/gem-push.yml"
91
+ - ".github/workflows/test.yml"
64
92
  - ".gitignore"
93
+ - AGENTS.md
94
+ - CHANGELOG.md
65
95
  - Gemfile
96
+ - Gemfile.lock
66
97
  - LICENSE
67
98
  - README.md
99
+ - RELEASING.md
100
+ - Rakefile
68
101
  - lib/yaml_exporter.rb
69
- - lib/yaml_serializable/structure_builder.rb
70
- - lib/yaml_serializable/version.rb
71
- - lib/yaml_serializable/yaml_exporter.rb
102
+ - lib/yaml_exporter/builder.rb
103
+ - lib/yaml_exporter/exporter.rb
104
+ - lib/yaml_exporter/importer.rb
105
+ - lib/yaml_exporter/nodes/attribute.rb
106
+ - lib/yaml_exporter/nodes/many_base.rb
107
+ - lib/yaml_exporter/nodes/many_find_by.rb
108
+ - lib/yaml_exporter/nodes/many_positional.rb
109
+ - lib/yaml_exporter/nodes/many_reference.rb
110
+ - lib/yaml_exporter/nodes/many_through.rb
111
+ - lib/yaml_exporter/nodes/one_owned.rb
112
+ - lib/yaml_exporter/nodes/one_reference.rb
113
+ - lib/yaml_exporter/nodes/one_reference_of.rb
114
+ - lib/yaml_exporter/schema.rb
115
+ - lib/yaml_exporter/structure.rb
116
+ - lib/yaml_exporter/type_inference.rb
117
+ - lib/yaml_exporter/version.rb
118
+ - script/demo_quiz.rb
72
119
  - yaml_exporter.gemspec
73
120
  homepage: https://github.com/itadventurer/yaml_exporter
74
121
  licenses:
@@ -76,7 +123,6 @@ licenses:
76
123
  metadata:
77
124
  homepage_uri: https://github.com/itadventurer/yaml_exporter
78
125
  source_code_uri: https://github.com/itadventurer/yaml_exporter
79
- post_install_message:
80
126
  rdoc_options: []
81
127
  require_paths:
82
128
  - lib
@@ -84,15 +130,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
130
  requirements:
85
131
  - - ">="
86
132
  - !ruby/object:Gem::Version
87
- version: 2.5.0
133
+ version: '3.2'
88
134
  required_rubygems_version: !ruby/object:Gem::Requirement
89
135
  requirements:
90
136
  - - ">="
91
137
  - !ruby/object:Gem::Version
92
138
  version: '0'
93
139
  requirements: []
94
- rubygems_version: 3.5.11
95
- signing_key:
140
+ rubygems_version: 3.6.9
96
141
  specification_version: 4
97
- summary: YAML serialization for ActiveRecord models with JSON schema support
142
+ summary: YAML import/export for ActiveRecord models with a small declarative DSL
98
143
  test_files: []
@@ -1,30 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- module YamlSerializable
2
- VERSION = "0.1.0"
3
- end
@@ -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