dami 1.0.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +306 -0
  3. data/LICENSE +21 -0
  4. data/README.md +106 -0
  5. data/bin/dami +10 -0
  6. data/docs/01.Getting_Started.md +220 -0
  7. data/docs/02.Models_and_Fields.md +244 -0
  8. data/docs/03.Querying.md +387 -0
  9. data/docs/04.Creating_Updating_Deleting.md +226 -0
  10. data/docs/05.Validation.md +259 -0
  11. data/docs/06.Protection.md +160 -0
  12. data/docs/07.Associations.md +239 -0
  13. data/docs/08.Scopes.md +99 -0
  14. data/docs/09.Migrations.md +165 -0
  15. data/docs/10.Flows_and_Commands.md +181 -0
  16. data/docs/11.Localization.md +435 -0
  17. data/docs/12.TheDamiWay.md +227 -0
  18. data/docs/Manifesto.md +305 -0
  19. data/docs/site.md +477 -0
  20. data/lib/dami/actions/command.rb +80 -0
  21. data/lib/dami/actions/context.rb +63 -0
  22. data/lib/dami/actions/draft.rb +36 -0
  23. data/lib/dami/actions/flow.rb +42 -0
  24. data/lib/dami/adapters/base.rb +40 -0
  25. data/lib/dami/adapters/sqlite/connection.rb +122 -0
  26. data/lib/dami/adapters/sqlite/core.rb +17 -0
  27. data/lib/dami/adapters/sqlite/query.rb +353 -0
  28. data/lib/dami/adapters/sqlite/schema.rb +135 -0
  29. data/lib/dami/cli.rb +117 -0
  30. data/lib/dami/configuration.rb +246 -0
  31. data/lib/dami/core.rb +98 -0
  32. data/lib/dami/dsl_guardrails.rb +45 -0
  33. data/lib/dami/errors.rb +89 -0
  34. data/lib/dami/inflector.rb +245 -0
  35. data/lib/dami/localization.rb +131 -0
  36. data/lib/dami/migration.rb +84 -0
  37. data/lib/dami/migrator.rb +105 -0
  38. data/lib/dami/plugins/associations.rb +284 -0
  39. data/lib/dami/plugins/nested_attributes.rb +180 -0
  40. data/lib/dami/plugins/protection.rb +30 -0
  41. data/lib/dami/plugins/validations.rb +90 -0
  42. data/lib/dami/query/builder.rb +132 -0
  43. data/lib/dami/query/enumerable.rb +62 -0
  44. data/lib/dami/query/persistence.rb +133 -0
  45. data/lib/dami/record_proxy.rb +32 -0
  46. data/lib/dami/result.rb +27 -0
  47. data/lib/dami/schema/diff.rb +84 -0
  48. data/lib/dami/schema/dumper.rb +60 -0
  49. data/lib/dami/schema/generator.rb +69 -0
  50. data/lib/dami/schema/introspector.rb +34 -0
  51. data/lib/dami/schema/loader.rb +58 -0
  52. data/lib/dami/schema.rb +13 -0
  53. data/lib/dami/validation_rules.rb +72 -0
  54. data/lib/dami/version.rb +3 -0
  55. data/lib/dami.rb +32 -0
  56. metadata +218 -0
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+ module Dami
3
+ module Inflector
4
+ # Caches for performance
5
+ @plural_cache = {}
6
+ @singular_cache = {}
7
+
8
+ UNCOUNTABLE = %w(
9
+ equipment information rice money species series fish sheep jeans police deer news homework
10
+ air water sugar tea coffee milk butter cheese bread jam chocolate wine beer music art
11
+ software love happiness furniture luggage advice work traffic weather accommodation
12
+ education knowledge research progress health safety violence peace chaos math physics
13
+ chemistry biology moose swine bison aircraft spacecraft salmon trout offspring
14
+ ).freeze
15
+
16
+ # These singular words end in 's' but are NOT plural!
17
+ SINGULAR_ENDING_IN_S = %w(
18
+ bus gas lens glass class mass grass brass canvas atlas bias cosmos dais iris oasis pancreas
19
+ alias status thesis basis crisis analysis diagnosis emphasis hypothesis neurosis osmosis
20
+ paralysis parenthesis synopsis
21
+ ).freeze
22
+
23
+ IRREGULAR = {
24
+ 'person' => 'people', 'man' => 'men', 'woman' => 'women', 'child' => 'children',
25
+ 'tooth' => 'teeth', 'foot' => 'feet', 'mouse' => 'mice', 'goose' => 'geese',
26
+ 'ox' => 'oxen', 'quiz' => 'quizzes', 'matrix' => 'matrices', 'vertex' => 'vertices',
27
+ 'index' => 'indices', 'sex' => 'sexes', 'move' => 'moves', 'zombie' => 'zombies',
28
+ 'cactus' => 'cacti', 'focus' => 'foci', 'fungus' => 'fungi', 'nucleus' => 'nuclei',
29
+ 'radius' => 'radii', 'stimulus' => 'stimuli', 'axis' => 'axes', 'analysis' => 'analyses',
30
+ 'basis' => 'bases', 'crisis' => 'crises', 'diagnosis' => 'diagnoses',
31
+ 'ellipsis' => 'ellipses', 'hypothesis' => 'hypotheses', 'oasis' => 'oases',
32
+ 'paralysis' => 'paralyses', 'parenthesis' => 'parentheses', 'synopsis' => 'synopses',
33
+ 'thesis' => 'theses', 'phenomenon' => 'phenomena', 'criterion' => 'criteria',
34
+ 'datum' => 'data',
35
+ # Add the common -s words as irregular to handle them explicitly
36
+ 'bus' => 'buses', 'gas' => 'gases', 'lens' => 'lenses', 'glass' => 'glasses',
37
+ 'class' => 'classes', 'mass' => 'masses', 'grass' => 'grasses', 'brass' => 'brasses',
38
+ 'canvas' => 'canvases', 'atlas' => 'atlases', 'bias' => 'biases', 'cosmos' => 'cosmoses',
39
+ 'dais' => 'daises', 'iris' => 'irises', 'pancreas' => 'pancreases',
40
+ 'alias' => 'aliases', 'status' => 'statuses'
41
+ }.freeze
42
+
43
+ IRREGULAR_REVERSE = IRREGULAR.invert.freeze
44
+
45
+ # Rules ordered from most specific to least specific
46
+ PLURAL_RULES = [
47
+ # Irregular patterns
48
+ [/^(ox)$/i, '\1en'],
49
+ [/^(m|l)ouse$/i, '\1ice'],
50
+ [/(quiz)$/i, '\1zes'],
51
+
52
+ # Words ending in s, ss, x, z, ch, sh
53
+ [/(ss)$/i, '\1es'],
54
+ [/(x|z|ch|sh)$/i, '\1es'],
55
+
56
+ # Words ending in o
57
+ [/(tomat|potat|ech|her|vet)o$/i, '\1oes'],
58
+ [/o$/i, 'os'],
59
+
60
+ # Words ending in f or fe
61
+ [/(lea|loa|thie|shel|wol|hal|cal|kni)f$/i, '\1ves'],
62
+ [/(wi|li)fe$/i, '\1ves'],
63
+
64
+ # Words ending in y
65
+ [/([^aeiou])y$/i, '\1ies'],
66
+ [/([aeiou]y)$/i, '\1s'],
67
+
68
+ # Words ending in is
69
+ [/(ax|test)is$/i, '\1es'],
70
+ [/sis$/i, 'ses'],
71
+
72
+ # Words ending in us
73
+ [/(octop|vir|radi|nucle|fung|cact|stimul)us$/i, '\1i'],
74
+ [/us$/i, 'uses'],
75
+
76
+ # Words ending in um
77
+ [/([ti])um$/i, '\1a'],
78
+
79
+ # Words ending in ix or ex
80
+ [/(matr|vert|ind)(ix|ex)$/i, '\1ices'],
81
+
82
+ # Single 's' at the end (but not ss)
83
+ [/([^s])s$/i, '\1ses'],
84
+
85
+ # Default: just add s
86
+ [/$/, 's']
87
+ ].freeze
88
+
89
+ SINGULAR_RULES = [
90
+ # Irregular patterns first
91
+ [/^oxen$/i, 'ox'],
92
+ [/^(m|l)ice$/i, '\1ouse'],
93
+ [/(quiz)zes$/i, '\1'],
94
+
95
+ # Words ending in sses
96
+ [/(ss)es$/i, '\1'],
97
+
98
+ # Words ending in xes, zes, ches, shes
99
+ [/(x|z|ch|sh)es$/i, '\1'],
100
+
101
+ # Words ending in oes
102
+ [/(tomat|potat|ech|her|vet)oes$/i, '\1o'],
103
+ [/oes$/i, 'o'],
104
+
105
+ # Words ending in ves
106
+ [/(lea|loa|thie|shel|wol|hal|cal|kni)ves$/i, '\1f'],
107
+ [/(wi|li)ves$/i, '\1fe'],
108
+
109
+ # Words ending in ies
110
+ [/([^aeiou])ies$/i, '\1y'],
111
+
112
+ # Words ending in uses (from -us)
113
+ [/([^aeiouy]|qu)uses$/i, '\1us'],
114
+
115
+ # Words ending in i (Latin plurals)
116
+ [/(octop|vir|radi|nucle|fung|cact|stimul)i$/i, '\1us'],
117
+
118
+ # Words ending in a (Greek/Latin plurals)
119
+ [/([ti])a$/i, '\1um'],
120
+
121
+ # Words ending in ices
122
+ [/(matr|vert|ind)ices$/i, '\1ex'],
123
+
124
+ # Words ending in es
125
+ [/(ax|test)es$/i, '\1is'],
126
+ [/ses$/i, 'sis'],
127
+
128
+ # Words ending in s (default - MUST be last)
129
+ [/s$/i, '']
130
+ ].freeze
131
+
132
+ def self.apply_case(original, new_word)
133
+ return new_word if original == new_word
134
+
135
+ if original == original.upcase
136
+ new_word.upcase
137
+ elsif original == original.capitalize
138
+ new_word.capitalize
139
+ elsif original[0] == original[0].upcase
140
+ new_word[0].upcase + new_word[1..-1]
141
+ else
142
+ new_word
143
+ end
144
+ end
145
+
146
+ def self.pluralize(word)
147
+ str = word.to_s.strip
148
+ return str if str.empty?
149
+ return @plural_cache[str] if @plural_cache.key?(str)
150
+
151
+ lower = str.downcase
152
+
153
+ # Uncountable
154
+ if UNCOUNTABLE.include?(lower)
155
+ return @plural_cache[str] = str
156
+ end
157
+
158
+ # Irregular singular -> plural (includes bus, gas, lens, etc.)
159
+ if irregular = IRREGULAR[lower]
160
+ return @plural_cache[str] = apply_case(str, irregular)
161
+ end
162
+
163
+ # Already irregular plural
164
+ if IRREGULAR_REVERSE.key?(lower)
165
+ return @plural_cache[str] = str
166
+ end
167
+
168
+ # Already plural check - a simple heuristic
169
+ # If it ends in 's' but isn't in our singular list, it might be plural
170
+ if lower.end_with?('s') && !lower.end_with?('ss') && !SINGULAR_ENDING_IN_S.include?(lower)
171
+ # Try to singularize and re-pluralize to check
172
+ test_singular = apply_singular_rules(str)
173
+ if test_singular != str
174
+ test_plural = apply_plural_rules(test_singular)
175
+ if test_plural.downcase == lower
176
+ return @plural_cache[str] = str # Already plural
177
+ end
178
+ end
179
+ end
180
+
181
+ # Apply plural rules
182
+ result = apply_plural_rules(str)
183
+ @plural_cache[str] = result
184
+ end
185
+
186
+ def self.singularize(word)
187
+ str = word.to_s.strip
188
+ return str if str.empty?
189
+ return @singular_cache[str] if @singular_cache.key?(str)
190
+
191
+ lower = str.downcase
192
+
193
+ # Uncountable
194
+ if UNCOUNTABLE.include?(lower)
195
+ return @singular_cache[str] = str
196
+ end
197
+
198
+ # Irregular plural -> singular (includes buses, gases, lenses, etc.)
199
+ if irregular = IRREGULAR_REVERSE[lower]
200
+ return @singular_cache[str] = apply_case(str, irregular)
201
+ end
202
+
203
+ # Already irregular singular
204
+ if IRREGULAR.key?(lower)
205
+ return @singular_cache[str] = str
206
+ end
207
+
208
+ # Check if it's a known singular word ending in 's'
209
+ if SINGULAR_ENDING_IN_S.include?(lower)
210
+ return @singular_cache[str] = str
211
+ end
212
+
213
+ # Already singular check - if it doesn't end in 's', it's probably singular
214
+ if !lower.end_with?('s')
215
+ return @singular_cache[str] = str
216
+ end
217
+
218
+ # Apply singular rules
219
+ result = apply_singular_rules(str)
220
+ @singular_cache[str] = result
221
+ end
222
+
223
+ private
224
+
225
+ def self.apply_plural_rules(str)
226
+ PLURAL_RULES.each do |regex, replacement|
227
+ if str =~ regex
228
+ result = str.sub(regex, replacement)
229
+ return apply_case(str, result)
230
+ end
231
+ end
232
+ str
233
+ end
234
+
235
+ def self.apply_singular_rules(str)
236
+ SINGULAR_RULES.each do |regex, replacement|
237
+ if str =~ regex
238
+ result = str.sub(regex, replacement)
239
+ return apply_case(str, result)
240
+ end
241
+ end
242
+ str
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,131 @@
1
+ # File: lib/dami/localization.rb
2
+ # frozen_string_literal: true
3
+ #raise "LOCALIZATION FILE RELOADED AT #{Time.now.to_f}"
4
+
5
+ module Dami
6
+ class << self
7
+ attr_reader :translations
8
+ end
9
+
10
+ @translations = {}
11
+ @locale = nil
12
+
13
+ # Process-wide default locale (set once at boot).
14
+ def self.locale
15
+ instance_variable_get(:@locale)
16
+ end
17
+
18
+ def self.locale=(new_locale)
19
+ instance_variable_set(:@locale, new_locale&.to_sym)
20
+ end
21
+
22
+ # Per-thread override. Web servers run requests on threads, so a request's
23
+ # locale must never leak into another request: set it here (or use
24
+ # with_locale), never via Dami.locale= from inside a request.
25
+ def self.thread_locale
26
+ Thread.current[:dami_locale]
27
+ end
28
+
29
+ def self.thread_locale=(new_locale)
30
+ Thread.current[:dami_locale] = new_locale&.to_sym
31
+ end
32
+
33
+ def self.current_locale
34
+ locale = Thread.current[:dami_locale] || instance_variable_get(:@locale)
35
+
36
+ # Only convert to symbol if locale exists and is not empty
37
+ return locale.to_sym if locale && !locale.to_s.empty?
38
+
39
+ if defined?(I18n) && I18n.respond_to?(:locale)
40
+ i18n_locale = I18n.locale
41
+ return i18n_locale.to_sym if i18n_locale
42
+ end
43
+
44
+ :en
45
+ end
46
+ def self.localize(scope, &block)
47
+ scope_storage = (@translations[scope] ||= {})
48
+ LocalizationProxy.new(scope_storage).instance_eval(&block)
49
+ end
50
+
51
+ def self.translate(key, *args)
52
+ keys = key.to_s.split('.').map(&:to_sym)
53
+ locale = current_locale
54
+
55
+ message = @translations.dig(*keys.insert(1, locale))
56
+ if message.is_a?(Proc)
57
+ message.call(*args)
58
+ else
59
+ message || "translation missing: #{locale}.#{key}"
60
+ end
61
+ end
62
+
63
+ # Thread-local: only the calling thread sees the temporary locale.
64
+ def self.with_locale(temp_locale, &block)
65
+ original_locale = Thread.current[:dami_locale]
66
+ Thread.current[:dami_locale] = temp_locale&.to_sym
67
+ yield
68
+ ensure
69
+ Thread.current[:dami_locale] = original_locale
70
+ end
71
+ # --- Helper Methods ---
72
+
73
+ def self.localize_model(model_name, locale: nil)
74
+ locale ||= current_locale
75
+ @translations.dig(:models, locale, model_name, :model_name)
76
+ end
77
+
78
+ class << self
79
+ alias_method :lm, :localize_model
80
+ end
81
+
82
+ def self.localize_field(model_name, field_name, locale: nil)
83
+ locale ||= current_locale
84
+ @translations.dig(:models, locale, model_name, field_name)
85
+ end
86
+
87
+ class << self
88
+ alias_method :lf, :localize_field
89
+ end
90
+
91
+ # --- DSL Proxy Classes ---
92
+
93
+ class LocalizationProxy
94
+ def initialize(storage); @storage = storage; end
95
+ def en(&block); locale(:en, &block); end
96
+ def es(&block); locale(:es, &block); end
97
+ def fr(&block); locale(:fr, &block); end
98
+ def de(&block); locale(:de, &block); end
99
+ def ja(&block); locale(:ja, &block); end
100
+ def zh(&block); locale(:zh, &block); end
101
+ def pt(&block); locale(:pt, &block); end
102
+ def it(&block); locale(:it, &block); end
103
+ def ru(&block); locale(:ru, &block); end
104
+ def ar(&block); locale(:ar, &block); end
105
+
106
+ def locale(lang_code, &block)
107
+ locale_storage = (@storage[lang_code.to_sym] ||= {})
108
+ LocaleProxy.new(locale_storage).instance_eval(&block)
109
+ end
110
+ end
111
+
112
+ class LocaleProxy
113
+ def initialize(storage); @storage = storage; end
114
+ def set(key, value); @storage[key.to_sym] = value; end
115
+
116
+ def attributes_for(model_name, &block)
117
+ model_storage = (@storage[model_name.to_sym] ||= {})
118
+ AttributesProxy.new(model_storage).instance_eval(&block)
119
+ end
120
+
121
+ def enum_for(model_name, attribute, &block)
122
+ enum_storage = ((@storage[model_name.to_sym] ||= {})[attribute.to_sym] = {})
123
+ AttributesProxy.new(enum_storage).instance_eval(&block)
124
+ end
125
+ end
126
+
127
+ class AttributesProxy
128
+ def initialize(storage); @storage = storage; end
129
+ def set(key, value); @storage[key.to_sym] = value; end
130
+ end
131
+ end
@@ -0,0 +1,84 @@
1
+ # lib/dami/migration.rb - Fix create_table to support block parameter syntax
2
+
3
+ module Dami
4
+ class Migration
5
+ def initialize(adapter)
6
+ @adapter = adapter
7
+ end
8
+
9
+ def up
10
+ # This should be overridden by the migration file
11
+ end
12
+
13
+ def down
14
+ # This should be overridden by the migration file
15
+ end
16
+
17
+ # Schema methods
18
+ def create_table(name, &block)
19
+ table_definition = TableDefinition.new
20
+
21
+ # Support both styles: do |t| ... end and do ... end
22
+ if block.arity == 0
23
+ table_definition.instance_eval(&block)
24
+ else
25
+ yield table_definition
26
+ end
27
+
28
+ @adapter.create_table(name, table_definition.columns)
29
+ end
30
+
31
+ def drop_table(name)
32
+ @adapter.execute("DROP TABLE IF EXISTS #{name}")
33
+ end
34
+
35
+ def add_column(table, column, type, **options)
36
+ @adapter.add_column(table, column, type, **options)
37
+ end
38
+
39
+ def remove_column(table, column)
40
+ # SQLite has limited DROP COLUMN support
41
+ # This will work in SQLite 3.35.0+ but fail gracefully in older versions
42
+ begin
43
+ @adapter.execute("ALTER TABLE #{table} DROP COLUMN #{column}")
44
+ rescue SQLite3::Exception => e
45
+ # Log the limitation but don't crash
46
+ puts "Note: DROP COLUMN not supported in this SQLite version: #{e.message}"
47
+ end
48
+ end
49
+ def add_index(table_name, column_name, **options)
50
+ @adapter.add_index(table_name, column_name, options)
51
+ end
52
+
53
+ def remove_index(table_name, column_name, **options)
54
+ @adapter.remove_index(table_name, column_name, options)
55
+ end
56
+
57
+
58
+ end
59
+
60
+ class TableDefinition
61
+ attr_reader :columns
62
+
63
+ def initialize
64
+ @columns = []
65
+ end
66
+
67
+ def field(name, type, **options)
68
+ @columns << { name: name, type: type, **options }
69
+ end
70
+
71
+ # created_at / updated_at columns. Dami fills them automatically on
72
+ # create/update when the model declares the same two fields.
73
+ def timestamps
74
+ field(:created_at, :datetime)
75
+ field(:updated_at, :datetime)
76
+ end
77
+
78
+ # Shorthand for a foreign-key column: t.references :user -> user_id INTEGER REFERENCES users(id)
79
+ def references(name, **options)
80
+ table = options.delete(:table) || Dami::Inflector.pluralize(name.to_s)
81
+ field(:"#{name}_id", :integer, references: table, **options)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,105 @@
1
+ # lib/dami/migrator.rb
2
+ # frozen_string_literal: true
3
+ module Dami
4
+ class Migrator
5
+ def initialize(adapter, path: Dami.configuration.migrations_path)
6
+ @adapter = adapter
7
+ @path = path
8
+ ensure_schema_migrations_table
9
+ end
10
+
11
+ def migrate
12
+ pending_migrations.each { |m| run_migration(m) }
13
+ dump_schema # ADD THIS LINE
14
+ end
15
+
16
+ def rollback(steps = 1)
17
+ completed_migrations.reverse.first(steps).each { |m| reverse_migration(m) }
18
+ dump_schema
19
+ end
20
+
21
+ private
22
+
23
+ def ensure_schema_migrations_table
24
+ return if @adapter.table_exists?(:schema_migrations)
25
+
26
+ @adapter.execute <<-SQL
27
+ CREATE TABLE schema_migrations (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ version VARCHAR(255) NOT NULL UNIQUE,
30
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
31
+ )
32
+ SQL
33
+ end
34
+
35
+ def all_migrations
36
+ return [] unless Dir.exist?(@path)
37
+ Dir.glob("#{@path}/*.rb").sort.map do |file|
38
+ version = File.basename(file).match(/^(\d+)_/)[1]
39
+ { version: version, file: file }
40
+ end
41
+ end
42
+
43
+ def completed_versions
44
+ query = {
45
+ model_name: :schema_migrations,
46
+ conditions: [],
47
+ order_by: nil,
48
+ limit: nil,
49
+ offset: nil
50
+ }
51
+ @adapter.query_records(query).map { |r| r[:version] }
52
+ end
53
+
54
+ def pending_migrations
55
+ all_migrations.reject { |m| completed_versions.include?(m[:version]) }
56
+ end
57
+
58
+ def completed_migrations
59
+ all_migrations.select { |m| completed_versions.include?(m[:version]) }
60
+ end
61
+
62
+ def load_migration(file)
63
+ migration = Migration.new(@adapter)
64
+ # This part is flexible; it will define up/down methods if they exist.
65
+ migration.instance_eval(File.read(file), file)
66
+ migration
67
+ end
68
+
69
+ def run_migration(migration_info)
70
+ migration = load_migration(migration_info[:file])
71
+ @adapter.transaction do
72
+ # THE FIX: Call the new method name.
73
+ migration.up
74
+ @adapter.insert_record(:schema_migrations, { version: migration_info[:version] })
75
+ end
76
+ end
77
+
78
+ def reverse_migration(migration_info)
79
+ migration = load_migration(migration_info[:file])
80
+ @adapter.transaction do
81
+ # THE FIX: Call the new method name.
82
+ migration.down
83
+
84
+ @adapter.execute(
85
+ "DELETE FROM schema_migrations WHERE version = ?",
86
+ [migration_info[:version]]
87
+ )
88
+ end
89
+ rescue => e
90
+ puts "Error in reverse_migration for #{migration_info[:version]}: #{e.message}"
91
+ puts e.backtrace.first(5)
92
+ raise
93
+ end
94
+ def dump_schema
95
+ require_relative 'schema/dumper'
96
+ dumper = Dami::Schema::Dumper.new(@adapter)
97
+ schema_content = dumper.dump
98
+
99
+ # Use the configured path.
100
+ schema_path = Dami.configuration.schema_path
101
+ FileUtils.mkdir_p(File.dirname(schema_path))
102
+ File.write(schema_path, schema_content)
103
+ end
104
+ end
105
+ end