declare_schema 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.
- checksums.yaml +4 -4
- data/.travis.yml +37 -0
- data/CHANGELOG.md +28 -4
- data/Gemfile +0 -2
- data/Gemfile.lock +1 -4
- data/README.md +59 -2
- data/Rakefile +13 -20
- data/gemfiles/rails_4.gemfile +4 -7
- data/gemfiles/rails_5.gemfile +4 -7
- data/gemfiles/rails_6.gemfile +4 -7
- data/lib/declare_schema/model.rb +0 -1
- data/lib/declare_schema/model/field_spec.rb +4 -14
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +20 -13
- data/lib/generators/declare_schema/migration/migrator.rb +58 -38
- data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
- data/lib/generators/declare_schema/support/eval_template.rb +12 -3
- data/lib/generators/declare_schema/support/model.rb +77 -2
- data/spec/lib/declare_schema/api_spec.rb +125 -0
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +8 -4
- data/spec/lib/declare_schema/generator_spec.rb +57 -0
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +51 -0
- data/spec/lib/declare_schema/migration_generator_spec.rb +686 -0
- data/spec/lib/declare_schema/prepare_testapp.rb +29 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +42 -0
- data/spec/spec_helper.rb +26 -0
- metadata +9 -12
- data/.jenkins/Jenkinsfile +0 -72
- data/.jenkins/ruby_build_pod.yml +0 -19
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
- data/test/api.rdoctest +0 -136
- data/test/doc-only.rdoctest +0 -76
- data/test/generators.rdoctest +0 -60
- data/test/interactive_primary_key.rdoctest +0 -56
- data/test/migration_generator.rdoctest +0 -846
- data/test/migration_generator_comments.rdoctestDISABLED +0 -74
- data/test/prepare_testapp.rb +0 -15
@@ -96,6 +96,20 @@ module Generators
|
|
96
96
|
def connection
|
97
97
|
ActiveRecord::Base.connection
|
98
98
|
end
|
99
|
+
|
100
|
+
def fix_native_types(types)
|
101
|
+
case connection.class.name
|
102
|
+
when /mysql/i
|
103
|
+
types[:integer][:limit] ||= 11
|
104
|
+
types[:text][:limit] ||= 0xffff
|
105
|
+
types[:binary][:limit] ||= 0xffff
|
106
|
+
end
|
107
|
+
types
|
108
|
+
end
|
109
|
+
|
110
|
+
def native_types
|
111
|
+
@native_types ||= fix_native_types(connection.native_database_types)
|
112
|
+
end
|
99
113
|
end
|
100
114
|
|
101
115
|
def initialize(ambiguity_resolver = {})
|
@@ -106,37 +120,29 @@ module Generators
|
|
106
120
|
|
107
121
|
attr_accessor :renames
|
108
122
|
|
123
|
+
# TODO: Add an application callback (maybe an initializer in a special group?) that
|
124
|
+
# the application can use to load other models that live in the database, to support DeclareSchema migrations
|
125
|
+
# for them.
|
109
126
|
def load_rails_models
|
127
|
+
ActiveRecord::Migration.verbose = false
|
128
|
+
|
110
129
|
Rails.application.eager_load!
|
130
|
+
Rails::Engine.subclasses.each(&:eager_load!)
|
111
131
|
end
|
112
132
|
|
113
133
|
# Returns an array of model classes that *directly* extend
|
114
134
|
# ActiveRecord::Base, excluding anything in the CGI module
|
115
135
|
def table_model_classes
|
116
136
|
load_rails_models
|
117
|
-
ActiveRecord::Base.send(:descendants).
|
137
|
+
ActiveRecord::Base.send(:descendants).select do |klass|
|
138
|
+
klass.base_class == klass && !klass.name.starts_with?("CGI::")
|
139
|
+
end
|
118
140
|
end
|
119
141
|
|
120
142
|
def connection
|
121
143
|
self.class.connection
|
122
144
|
end
|
123
145
|
|
124
|
-
class << self
|
125
|
-
def fix_native_types(types)
|
126
|
-
case connection.class.name
|
127
|
-
when /mysql/i
|
128
|
-
types[:integer][:limit] ||= 11
|
129
|
-
types[:text][:limit] ||= 0xffff
|
130
|
-
types[:binary][:limit] ||= 0xffff
|
131
|
-
end
|
132
|
-
types
|
133
|
-
end
|
134
|
-
|
135
|
-
def native_types
|
136
|
-
@native_types ||= fix_native_types(connection.native_database_types)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
146
|
def native_types
|
141
147
|
self.class.native_types
|
142
148
|
end
|
@@ -217,14 +223,24 @@ module Generators
|
|
217
223
|
end
|
218
224
|
end
|
219
225
|
|
220
|
-
def always_ignore_tables
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
226
|
+
def self.always_ignore_tables
|
227
|
+
sessions_table =
|
228
|
+
begin
|
229
|
+
if defined?(CGI::Session::ActiveRecordStore::Session) &&
|
230
|
+
defined?(ActionController::Base) &&
|
231
|
+
ActionController::Base.session_store == CGI::Session::ActiveRecordStore
|
232
|
+
CGI::Session::ActiveRecordStore::Session.table_name
|
233
|
+
end
|
234
|
+
rescue
|
235
|
+
nil
|
236
|
+
end
|
237
|
+
|
238
|
+
[
|
239
|
+
'schema_info',
|
240
|
+
ActiveRecord::Base.try(:schema_migrations_table_name) || 'schema_migrations',
|
241
|
+
ActiveRecord::Base.try(:internal_metadata_table_name) || 'ar_internal_metadata',
|
242
|
+
sessions_table
|
243
|
+
].compact
|
228
244
|
end
|
229
245
|
|
230
246
|
def generate
|
@@ -246,7 +262,7 @@ module Generators
|
|
246
262
|
model_table_names = models_by_table_name.keys
|
247
263
|
|
248
264
|
to_create = model_table_names - db_tables
|
249
|
-
to_drop = db_tables - model_table_names - always_ignore_tables
|
265
|
+
to_drop = db_tables - model_table_names - self.class.always_ignore_tables
|
250
266
|
to_change = model_table_names
|
251
267
|
to_rename = extract_table_renames!(to_create, to_drop)
|
252
268
|
|
@@ -295,10 +311,6 @@ module Generators
|
|
295
311
|
down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes].flatten.reject(&:blank?) * "\n\n"
|
296
312
|
|
297
313
|
[up, down]
|
298
|
-
rescue Exception => ex
|
299
|
-
puts "Caught exception: #{ex}"
|
300
|
-
puts ex.backtrace.join("\n")
|
301
|
-
raise
|
302
314
|
end
|
303
315
|
|
304
316
|
def create_table(model)
|
@@ -398,14 +410,13 @@ module Generators
|
|
398
410
|
spec = model.field_specs[c]
|
399
411
|
if spec.different_to?(col) # TODO: DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
|
400
412
|
change_spec = fk_field_options(model, c)
|
401
|
-
change_spec[:limit]
|
413
|
+
change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
|
402
414
|
::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
|
403
415
|
(spec.limit || col.limit)
|
404
416
|
change_spec[:precision] = spec.precision unless spec.precision.nil?
|
405
417
|
change_spec[:scale] = spec.scale unless spec.scale.nil?
|
406
418
|
change_spec[:null] = spec.null unless spec.null && col.null
|
407
419
|
change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
|
408
|
-
change_spec[:comment] = spec.comment unless spec.comment.nil? && (col.comment if col.respond_to?(:comment)).nil?
|
409
420
|
|
410
421
|
changes << "change_column :#{new_table_name}, :#{c}, " +
|
411
422
|
([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
|
@@ -473,7 +484,7 @@ module Generators
|
|
473
484
|
return [[], []] if Migrator.disable_indexing
|
474
485
|
|
475
486
|
new_table_name = model.table_name
|
476
|
-
existing_fks = DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
|
487
|
+
existing_fks = ::DeclareSchema::Model::ForeignKeySpec.for_model(model, old_table_name)
|
477
488
|
model_fks = model.constraint_specs
|
478
489
|
add_fks = model_fks - existing_fks
|
479
490
|
drop_fks = existing_fks - model_fks
|
@@ -505,8 +516,7 @@ module Generators
|
|
505
516
|
next if k == :null && v == true
|
506
517
|
end
|
507
518
|
|
508
|
-
next if k == :limit && type == :text &&
|
509
|
-
(!::DeclareSchema::Model::FieldSpec.mysql_text_limits? || v == ::DeclareSchema::Model::FieldSpec::MYSQL_LONGTEXT_LIMIT)
|
519
|
+
next if k == :limit && type == :text && !::DeclareSchema::Model::FieldSpec.mysql_text_limits?
|
510
520
|
|
511
521
|
if k.is_a?(Symbol)
|
512
522
|
"#{k}: #{v.inspect}"
|
@@ -517,10 +527,20 @@ module Generators
|
|
517
527
|
end
|
518
528
|
|
519
529
|
def fk_field_options(model, field_name)
|
520
|
-
|
530
|
+
foreign_key = model.constraint_specs.find { |fk| field_name == fk.foreign_key.to_s }
|
531
|
+
if foreign_key && (parent_table = foreign_key.parent_table_name)
|
521
532
|
parent_columns = connection.columns(parent_table) rescue []
|
522
|
-
|
523
|
-
|
533
|
+
pk_limit =
|
534
|
+
if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
|
535
|
+
if Rails::VERSION::MAJOR <= 4
|
536
|
+
pk_column.cast_type.limit
|
537
|
+
else
|
538
|
+
pk_column.limit
|
539
|
+
end
|
540
|
+
else
|
541
|
+
8
|
542
|
+
end
|
543
|
+
|
524
544
|
{ limit: pk_limit }
|
525
545
|
else
|
526
546
|
{}
|
@@ -9,9 +9,18 @@ module DeclareSchema
|
|
9
9
|
private
|
10
10
|
|
11
11
|
def eval_template(template_name)
|
12
|
-
source
|
13
|
-
|
14
|
-
|
12
|
+
source = File.expand_path(find_in_source_paths(template_name))
|
13
|
+
erb = ERB.new(::File.read(source).force_encoding(Encoding::UTF_8), trim_mode: '>')
|
14
|
+
erb.filename = source
|
15
|
+
begin
|
16
|
+
erb.result(binding)
|
17
|
+
rescue Exception => ex
|
18
|
+
raise ex.class, <<~EOS
|
19
|
+
#{ex.message}
|
20
|
+
#{erb.src}
|
21
|
+
#{ex.backtrace.join("\n ")}
|
22
|
+
EOS
|
23
|
+
end
|
15
24
|
end
|
16
25
|
end
|
17
26
|
end
|
@@ -4,6 +4,39 @@ require_relative './eval_template'
|
|
4
4
|
|
5
5
|
module DeclareSchema
|
6
6
|
module Support
|
7
|
+
class IndentedBuffer
|
8
|
+
def initialize(indent: 0)
|
9
|
+
@string = +""
|
10
|
+
@indent = indent
|
11
|
+
@column = 0
|
12
|
+
@indent_amount = 2
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_string
|
16
|
+
@string
|
17
|
+
end
|
18
|
+
|
19
|
+
def indent!
|
20
|
+
@indent += @indent_amount
|
21
|
+
yield
|
22
|
+
@indent -= @indent_amount
|
23
|
+
end
|
24
|
+
|
25
|
+
def newline!
|
26
|
+
@column = 0
|
27
|
+
@string << "\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
def <<(str)
|
31
|
+
if (difference = @indent - @column) > 0
|
32
|
+
@string << ' ' * difference
|
33
|
+
end
|
34
|
+
@column += difference
|
35
|
+
@string << str
|
36
|
+
newline!
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
7
40
|
module Model
|
8
41
|
class << self
|
9
42
|
def included(base)
|
@@ -26,9 +59,51 @@ module DeclareSchema
|
|
26
59
|
|
27
60
|
def inject_declare_schema_code_into_model_file
|
28
61
|
gsub_file(model_path, / # attr_accessible :title, :body\n/m, "")
|
29
|
-
inject_into_class
|
30
|
-
|
62
|
+
inject_into_class(model_path, class_name) do
|
63
|
+
declare_model_fields_and_associations
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def declare_model_fields_and_associations
|
70
|
+
buffer = ::DeclareSchema::Support::IndentedBuffer.new(indent: 2)
|
71
|
+
buffer.newline!
|
72
|
+
buffer << 'fields do'
|
73
|
+
buffer.indent! do
|
74
|
+
field_attributes.each do |attribute|
|
75
|
+
decl = "%-#{max_attribute_length}s" % attribute.name + ' ' +
|
76
|
+
attribute.type.to_sym.inspect +
|
77
|
+
case attribute.type.to_s
|
78
|
+
when 'string'
|
79
|
+
', limit: 255'
|
80
|
+
else
|
81
|
+
''
|
82
|
+
end
|
83
|
+
buffer << decl
|
84
|
+
end
|
85
|
+
if options[:timestamps]
|
86
|
+
buffer.newline!
|
87
|
+
buffer << 'timestamps'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
buffer << 'end'
|
91
|
+
|
92
|
+
if bts.any?
|
93
|
+
buffer.newline!
|
94
|
+
bts.each do |bt|
|
95
|
+
buffer << "belongs_to #{bt.to_sym.inspect}"
|
96
|
+
end
|
31
97
|
end
|
98
|
+
if hms.any?
|
99
|
+
buffer.newline
|
100
|
+
hms.each do |hm|
|
101
|
+
buffer << "has_many #{hm.to_sym.inspect}, dependent: :destroy"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
buffer.newline!
|
105
|
+
|
106
|
+
buffer.to_string
|
32
107
|
end
|
33
108
|
|
34
109
|
protected
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'rails/generators'
|
5
|
+
|
6
|
+
RSpec.describe 'DeclareSchema API' do
|
7
|
+
before do
|
8
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'example models' do
|
12
|
+
it 'generates a model' do
|
13
|
+
expect(system("bundle exec rails generate declare_schema:model advert title:string body:text")).to be_truthy
|
14
|
+
|
15
|
+
# The above will generate the test, fixture and a model file like this:
|
16
|
+
# model_declaration = Rails::Generators.invoke('declare_schema:model', ['advert2', 'title:string', 'body:text'])
|
17
|
+
# expect(model_declaration.first).to eq([["Advert"], nil, "app/models/advert.rb", nil,
|
18
|
+
# [["AdvertTest"], "test/models/advert_test.rb", nil, "test/fixtures/adverts.yml"]])
|
19
|
+
|
20
|
+
expect(File.read("#{TESTAPP_PATH}/app/models/advert.rb")).to eq(<<~EOS)
|
21
|
+
class Advert < #{active_record_base_class}
|
22
|
+
|
23
|
+
fields do
|
24
|
+
title :string, limit: 255
|
25
|
+
body :text
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
EOS
|
30
|
+
system("rm -rf #{TESTAPP_PATH}/app/models/advert2.rb #{TESTAPP_PATH}/test/models/advert2.rb #{TESTAPP_PATH}/test/fixtures/advert2.rb")
|
31
|
+
|
32
|
+
# The migration generator uses this information to create a migration.
|
33
|
+
# The following creates and runs the migration:
|
34
|
+
|
35
|
+
expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
|
36
|
+
|
37
|
+
# We're now ready to start demonstrating the API
|
38
|
+
|
39
|
+
Rails.application.config.autoload_paths += ["#{TESTAPP_PATH}/app/models"]
|
40
|
+
|
41
|
+
$LOAD_PATH << "#{TESTAPP_PATH}/app/models"
|
42
|
+
|
43
|
+
unless Rails::VERSION::MAJOR >= 6
|
44
|
+
# TODO: get this to work on Travis for Rails 6
|
45
|
+
Rails::Generators.invoke('declare_schema:migration', %w[-n -m])
|
46
|
+
end
|
47
|
+
|
48
|
+
require 'advert'
|
49
|
+
|
50
|
+
## The Basics
|
51
|
+
|
52
|
+
# The main feature of DeclareSchema, aside from the migration generator, is the ability to declare rich types for your fields. For example, you can declare that a field is an email address, and the field will be automatically validated for correct email address syntax.
|
53
|
+
|
54
|
+
### Field Types
|
55
|
+
|
56
|
+
# Field values are returned as the type you specify.
|
57
|
+
|
58
|
+
Advert.destroy_all
|
59
|
+
|
60
|
+
a = Advert.new(body: "This is the body", id: 1, title: "title")
|
61
|
+
expect(a.body).to eq("This is the body")
|
62
|
+
|
63
|
+
# This also works after a round-trip to the database
|
64
|
+
|
65
|
+
a.save!
|
66
|
+
expect(a.reload.body).to eq("This is the body")
|
67
|
+
|
68
|
+
## Names vs. Classes
|
69
|
+
|
70
|
+
## Model extensions
|
71
|
+
|
72
|
+
# DeclareSchema adds a few features to your models.
|
73
|
+
|
74
|
+
### `Model.attr_type`
|
75
|
+
|
76
|
+
# Returns the type (i.e. class) declared for a given field or attribute
|
77
|
+
|
78
|
+
Advert.connection.schema_cache.clear!
|
79
|
+
Advert.reset_column_information
|
80
|
+
|
81
|
+
expect(Advert.attr_type(:title)).to eq(String)
|
82
|
+
expect(Advert.attr_type(:body)).to eq(String)
|
83
|
+
|
84
|
+
## Field validations
|
85
|
+
|
86
|
+
# DeclareSchema gives you some shorthands for declaring some common validations right in the field declaration
|
87
|
+
|
88
|
+
### Required fields
|
89
|
+
|
90
|
+
# The `:required` argument to a field gives a `validates_presence_of`:
|
91
|
+
|
92
|
+
class AdvertWithRequiredTitle < ActiveRecord::Base
|
93
|
+
self.table_name = 'adverts'
|
94
|
+
|
95
|
+
fields do
|
96
|
+
title :string, :required, limit: 255
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
a = AdvertWithRequiredTitle.new
|
101
|
+
expect(a.valid? || a.errors.full_messages).to eq(["Title can't be blank"])
|
102
|
+
a.id = 2
|
103
|
+
a.body = "hello"
|
104
|
+
a.title = "Jimbo"
|
105
|
+
a.save!
|
106
|
+
|
107
|
+
### Unique fields
|
108
|
+
|
109
|
+
# The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
|
110
|
+
|
111
|
+
class AdvertWithUniqueTitle < ActiveRecord::Base
|
112
|
+
self.table_name = 'adverts'
|
113
|
+
|
114
|
+
fields do
|
115
|
+
title :string, :unique, limit: 255
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
a = AdvertWithUniqueTitle.new :title => "Jimbo", id: 3, body: "hello"
|
120
|
+
expect(a.valid? || a.errors.full_messages).to eq(["Title has already been taken"])
|
121
|
+
a.title = "Sambo"
|
122
|
+
a.save!
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -3,11 +3,15 @@
|
|
3
3
|
require_relative '../../../lib/declare_schema/field_declaration_dsl'
|
4
4
|
|
5
5
|
RSpec.describe DeclareSchema::FieldDeclarationDsl do
|
6
|
-
|
7
|
-
|
8
|
-
name :string, limit: 127
|
6
|
+
before do
|
7
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
9
8
|
|
10
|
-
|
9
|
+
class TestModel < ActiveRecord::Base
|
10
|
+
fields do
|
11
|
+
name :string, limit: 127
|
12
|
+
|
13
|
+
timestamps
|
14
|
+
end
|
11
15
|
end
|
12
16
|
end
|
13
17
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe 'DeclareSchema Migration Generator' do
|
4
|
+
before do
|
5
|
+
load File.expand_path('prepare_testapp.rb', __dir__)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "generates nested models" do
|
9
|
+
Rails::Generators.invoke('declare_schema:model', %w[alpha/beta one:string two:integer])
|
10
|
+
|
11
|
+
expect(File.exist?('app/models/alpha/beta.rb')).to be_truthy
|
12
|
+
|
13
|
+
expect(File.read('app/models/alpha/beta.rb')).to eq(<<~EOS)
|
14
|
+
class Alpha::Beta < #{active_record_base_class}
|
15
|
+
|
16
|
+
fields do
|
17
|
+
one :string, limit: 255
|
18
|
+
two :integer
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
EOS
|
23
|
+
|
24
|
+
expect(File.read('app/models/alpha.rb')).to eq(<<~EOS)
|
25
|
+
module Alpha
|
26
|
+
def self.table_name_prefix
|
27
|
+
'alpha_'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
EOS
|
31
|
+
|
32
|
+
expect(File.read('test/models/alpha/beta_test.rb')).to eq(<<~EOS)
|
33
|
+
require 'test_helper'
|
34
|
+
|
35
|
+
class Alpha::BetaTest < ActiveSupport::TestCase
|
36
|
+
# test "the truth" do
|
37
|
+
# assert true
|
38
|
+
# end
|
39
|
+
end
|
40
|
+
EOS
|
41
|
+
|
42
|
+
expect(File.exist?('test/fixtures/alpha/beta.yml')).to be_truthy
|
43
|
+
|
44
|
+
$LOAD_PATH << "#{TESTAPP_PATH}/app/models"
|
45
|
+
|
46
|
+
expect(system("bundle exec rails generate declare_schema:migration -n -m")).to be_truthy
|
47
|
+
|
48
|
+
expect(File.exist?('db/schema.rb')).to be_truthy
|
49
|
+
|
50
|
+
expect(File.exist?("db/development.sqlite3") || File.exist?("db/test.sqlite3")).to be_truthy
|
51
|
+
|
52
|
+
module Alpha; end
|
53
|
+
require 'alpha/beta'
|
54
|
+
|
55
|
+
expect { Alpha::Beta }.to_not raise_exception
|
56
|
+
end
|
57
|
+
end
|