declare_schema 0.1.2 → 0.3.0.pre.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +1 -3
- data/README.md +20 -0
- data/Rakefile +13 -20
- data/gemfiles/rails_4.gemfile +0 -1
- data/gemfiles/rails_5.gemfile +0 -1
- data/gemfiles/rails_6.gemfile +0 -1
- data/lib/declare_schema/model.rb +0 -1
- data/lib/declare_schema/model/field_spec.rb +1 -12
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +20 -14
- data/lib/generators/declare_schema/migration/migrator.rb +54 -30
- 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 +72 -0
- data/spec/spec_helper.rb +26 -0
- metadata +11 -12
- data/lib/generators/declare_schema/model/templates/model_injection.rb.erb +0 -25
- data/test/api.rdoctest +0 -136
- data/test/generators.rdoctest +0 -62
- 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
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'tmpdir'
|
5
|
+
|
6
|
+
TESTAPP_PATH = ENV['TESTAPP_PATH'] || File.join(Dir.tmpdir, 'declare_schema_testapp') unless defined?(TESTAPP_PATH)
|
7
|
+
FileUtils.chdir(TESTAPP_PATH)
|
8
|
+
|
9
|
+
system "rm -rf app/models/ad* app/models/alpha*"
|
10
|
+
system "rm -rf test/models/ad* test/models/alpha*"
|
11
|
+
system "rm -rf test/fixtures/ad* test/fixtures/alpha*"
|
12
|
+
system "rm -rf db/migrate/*"
|
13
|
+
system "mkdir -p #{TESTAPP_PATH}/app/assets/config"
|
14
|
+
system "echo '' >> #{TESTAPP_PATH}/app/assets/config/manifest.js"
|
15
|
+
|
16
|
+
require "#{TESTAPP_PATH}/config/environment"
|
17
|
+
|
18
|
+
require 'rails/generators'
|
19
|
+
Rails::Generators.configure!(Rails.application.config.generators)
|
20
|
+
|
21
|
+
(ActiveRecord::Base.connection.tables - Generators::DeclareSchema::Migration::Migrator.always_ignore_tables).each do |table|
|
22
|
+
ActiveRecord::Base.connection.execute("DROP TABLE #{ActiveRecord::Base.connection.quote_table_name(table)}")
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveRecord::Base.send(:descendants).each do |model|
|
26
|
+
unless model.name['Active'] || model.name['Application']
|
27
|
+
nuke_model_class(model)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails'
|
4
|
+
require 'rails/generators'
|
5
|
+
|
6
|
+
module Generators
|
7
|
+
module DeclareSchema
|
8
|
+
module Migration
|
9
|
+
RSpec.describe Migrator do
|
10
|
+
before do
|
11
|
+
ActiveRecord::Base.connection.tables
|
12
|
+
end
|
13
|
+
|
14
|
+
subject { described_class.new }
|
15
|
+
|
16
|
+
describe 'format_options' do
|
17
|
+
let(:mysql_longtext_limit) { 0xffff_ffff }
|
18
|
+
|
19
|
+
context 'MySQL' do
|
20
|
+
before do
|
21
|
+
expect(::DeclareSchema::Model::FieldSpec).to receive(:mysql_text_limits?).and_return(true)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'returns text limits' do
|
25
|
+
expect(subject.format_options({ limit: mysql_longtext_limit }, :text)).to eq(["limit: #{mysql_longtext_limit}"])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'non-MySQL' do
|
30
|
+
before do
|
31
|
+
expect(::DeclareSchema::Model::FieldSpec).to receive(:mysql_text_limits?).and_return(false)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns text limits' do
|
35
|
+
expect(subject.format_options({ limit: mysql_longtext_limit }, :text)).to eq([])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#before_generating_migration' do
|
41
|
+
it 'requires a block be passed' do
|
42
|
+
expect { described_class.before_generating_migration }.to raise_error(ArgumentError, 'A block is required when setting the before_generating_migration callback')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'load_rails_models' do
|
47
|
+
before do
|
48
|
+
expect(Rails.application).to receive(:eager_load!)
|
49
|
+
expect(Rails::Engine).to receive(:subclasses).and_return([])
|
50
|
+
end
|
51
|
+
|
52
|
+
subject { described_class.new.load_rails_models }
|
53
|
+
|
54
|
+
context 'when a before_generating_migration callback is configured' do
|
55
|
+
let(:dummy_proc) { -> {} }
|
56
|
+
|
57
|
+
before do
|
58
|
+
described_class.before_generating_migration(&dummy_proc)
|
59
|
+
expect(dummy_proc).to receive(:call).and_return(true)
|
60
|
+
end
|
61
|
+
|
62
|
+
it { should be_truthy }
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'when no before_generating_migration callback is configured' do
|
66
|
+
it { should be_nil }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -22,6 +22,32 @@ RSpec.configure do |config|
|
|
22
22
|
|
23
23
|
RSpec::Support::ObjectFormatter.default_instance.max_formatted_output_length = 2_000
|
24
24
|
|
25
|
+
def active_record_base_class
|
26
|
+
if Rails::VERSION::MAJOR == 4
|
27
|
+
'ActiveRecord::Base'
|
28
|
+
else
|
29
|
+
'ApplicationRecord'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def migrate(renames = {})
|
34
|
+
up, down = Generators::DeclareSchema::Migration::Migrator.run(renames)
|
35
|
+
ActiveRecord::Migration.class_eval(up)
|
36
|
+
ActiveRecord::Base.send(:descendants).each { |model| model.reset_column_information }
|
37
|
+
[up, down]
|
38
|
+
end
|
39
|
+
|
40
|
+
def nuke_model_class(klass)
|
41
|
+
ActiveSupport::DescendantsTracker.instance_eval do
|
42
|
+
direct_descendants = class_variable_get('@@direct_descendants')
|
43
|
+
direct_descendants[ActiveRecord::Base] = direct_descendants[ActiveRecord::Base].to_a.reject { |descendant| descendant == klass }
|
44
|
+
if defined?(ApplicationRecord)
|
45
|
+
direct_descendants[ApplicationRecord] = direct_descendants[ApplicationRecord].to_a.reject { |descendant| descendant == klass }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
Object.instance_eval { remove_const(klass.name.to_sym) rescue nil }
|
49
|
+
end
|
50
|
+
|
25
51
|
def with_modified_env(options, &block)
|
26
52
|
ClimateControl.modify(options, &block)
|
27
53
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: declare_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0.pre.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Invoca Development adapted from hobo_fields by Tom Locke
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -68,24 +68,23 @@ files:
|
|
68
68
|
- lib/generators/declare_schema/migration/templates/migration.rb.erb
|
69
69
|
- lib/generators/declare_schema/model/USAGE
|
70
70
|
- lib/generators/declare_schema/model/model_generator.rb
|
71
|
-
- lib/generators/declare_schema/model/templates/model_injection.rb.erb
|
72
71
|
- lib/generators/declare_schema/support/eval_template.rb
|
73
72
|
- lib/generators/declare_schema/support/model.rb
|
74
73
|
- lib/generators/declare_schema/support/thor_shell.rb
|
74
|
+
- spec/lib/declare_schema/api_spec.rb
|
75
75
|
- spec/lib/declare_schema/field_declaration_dsl_spec.rb
|
76
|
+
- spec/lib/declare_schema/generator_spec.rb
|
77
|
+
- spec/lib/declare_schema/interactive_primary_key_spec.rb
|
78
|
+
- spec/lib/declare_schema/migration_generator_spec.rb
|
79
|
+
- spec/lib/declare_schema/prepare_testapp.rb
|
80
|
+
- spec/lib/generators/declare_schema/migration/migrator_spec.rb
|
76
81
|
- spec/spec_helper.rb
|
77
|
-
- test/api.rdoctest
|
78
|
-
- test/generators.rdoctest
|
79
|
-
- test/interactive_primary_key.rdoctest
|
80
|
-
- test/migration_generator.rdoctest
|
81
|
-
- test/migration_generator_comments.rdoctestDISABLED
|
82
|
-
- test/prepare_testapp.rb
|
83
82
|
- test_responses.txt
|
84
83
|
homepage: https://github.com/Invoca/declare_schema
|
85
84
|
licenses: []
|
86
85
|
metadata:
|
87
86
|
allowed_push_host: https://rubygems.org
|
88
|
-
post_install_message:
|
87
|
+
post_install_message:
|
89
88
|
rdoc_options: []
|
90
89
|
require_paths:
|
91
90
|
- lib
|
@@ -101,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
100
|
version: 1.3.6
|
102
101
|
requirements: []
|
103
102
|
rubygems_version: 3.0.3
|
104
|
-
signing_key:
|
103
|
+
signing_key:
|
105
104
|
specification_version: 4
|
106
105
|
summary: Database migration generator for Rails
|
107
106
|
test_files: []
|
@@ -1,25 +0,0 @@
|
|
1
|
-
|
2
|
-
fields do
|
3
|
-
<% for attribute in field_attributes -%>
|
4
|
-
<%= "%-#{max_attribute_length}s" % attribute.name %> :<%= attribute.type %><%=
|
5
|
-
case attribute.type.to_s
|
6
|
-
when 'string'
|
7
|
-
', limit: 255'
|
8
|
-
else
|
9
|
-
''
|
10
|
-
end
|
11
|
-
%>
|
12
|
-
<% end -%>
|
13
|
-
<% if options[:timestamps] -%>
|
14
|
-
timestamps
|
15
|
-
<% end -%>
|
16
|
-
end
|
17
|
-
|
18
|
-
<% for bt in bts -%>
|
19
|
-
belongs_to :<%= bt %>
|
20
|
-
<% end -%>
|
21
|
-
<%= "\n" unless bts.empty? -%>
|
22
|
-
<% for hm in hms -%>
|
23
|
-
has_many :<%= hm %>, dependent: :destroy
|
24
|
-
<% end -%>
|
25
|
-
<%= "\n" unless hms.empty? -%>
|
data/test/api.rdoctest
DELETED
@@ -1,136 +0,0 @@
|
|
1
|
-
# DeclareSchema API
|
2
|
-
|
3
|
-
In order for the API examples to run we need to load the rails generators of our testapp:
|
4
|
-
{.hidden}
|
5
|
-
|
6
|
-
doctest: prepare testapp environment
|
7
|
-
doctest_require: 'prepare_testapp'
|
8
|
-
{.hidden}
|
9
|
-
|
10
|
-
## Example Models
|
11
|
-
|
12
|
-
Let's define some example models that we can use to demonstrate the API. With DeclareSchema we can use the 'declare_schema:model' generator like so:
|
13
|
-
|
14
|
-
$ rails generate declare_schema:model advert title:string body:text
|
15
|
-
|
16
|
-
This will generate the test, fixture and a model file like this:
|
17
|
-
|
18
|
-
>> Rails::Generators.invoke 'declare_schema:model', %w(advert title:string body:text)
|
19
|
-
{.hidden}
|
20
|
-
|
21
|
-
class Advert < ActiveRecord::Base
|
22
|
-
fields do
|
23
|
-
title :string
|
24
|
-
body :text, limit: 0xffff, null: true
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
The migration generator uses this information to create a migration. The following creates and runs the migration so we're ready to go.
|
29
|
-
|
30
|
-
$ rails generate declare_schema:migration -n -m
|
31
|
-
|
32
|
-
We're now ready to start demonstrating the API
|
33
|
-
|
34
|
-
>> require_relative "#{Rails.root}/app/models/advert.rb" if Rails::VERSION::MAJOR > 5
|
35
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
36
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
37
|
-
{.hidden}
|
38
|
-
|
39
|
-
## The Basics
|
40
|
-
|
41
|
-
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.
|
42
|
-
|
43
|
-
### Field Types
|
44
|
-
|
45
|
-
Field values are returned as the type you specify.
|
46
|
-
|
47
|
-
>> a = Advert.new :body => "This is the body", id: 1, title: "title"
|
48
|
-
>> a.body.class
|
49
|
-
=> String
|
50
|
-
|
51
|
-
This also works after a round-trip to the database
|
52
|
-
|
53
|
-
>> a.save
|
54
|
-
>> b = Advert.find(a.id)
|
55
|
-
>> b.body.class
|
56
|
-
=> String
|
57
|
-
|
58
|
-
## Names vs. Classes
|
59
|
-
|
60
|
-
The full set of available symbolic names is
|
61
|
-
|
62
|
-
* `:integer`
|
63
|
-
* `:float`
|
64
|
-
* `:decimal`
|
65
|
-
* `:string`
|
66
|
-
* `:text`
|
67
|
-
* `:boolean`
|
68
|
-
* `:date`
|
69
|
-
* `:datetime`
|
70
|
-
* `:html`
|
71
|
-
* `:textile`
|
72
|
-
* `:markdown`
|
73
|
-
* `:password`
|
74
|
-
|
75
|
-
You can add your own types too. More on that later.
|
76
|
-
|
77
|
-
|
78
|
-
## Model extensions
|
79
|
-
|
80
|
-
DeclareSchema adds a few features to your models.
|
81
|
-
|
82
|
-
### `Model.attr_type`
|
83
|
-
|
84
|
-
Returns the type (i.e. class) declared for a given field or attribute
|
85
|
-
|
86
|
-
>> Advert.connection.schema_cache.clear!
|
87
|
-
>> Advert.reset_column_information
|
88
|
-
>> Advert.attr_type :title
|
89
|
-
=> String
|
90
|
-
>> Advert.attr_type :body
|
91
|
-
=> String
|
92
|
-
|
93
|
-
## Field validations
|
94
|
-
|
95
|
-
DeclareSchema gives you some shorthands for declaring some common validations right in the field declaration
|
96
|
-
|
97
|
-
### Required fields
|
98
|
-
|
99
|
-
The `:required` argument to a field gives a `validates_presence_of`:
|
100
|
-
|
101
|
-
>>
|
102
|
-
class Advert
|
103
|
-
fields do
|
104
|
-
title :string, :required, limit: 255
|
105
|
-
end
|
106
|
-
end
|
107
|
-
>> a = Advert.new
|
108
|
-
>> a.valid?
|
109
|
-
=> false
|
110
|
-
>> a.errors.full_messages
|
111
|
-
=> ["Title can't be blank"]
|
112
|
-
>> a.id = 2
|
113
|
-
>> a.body = "hello"
|
114
|
-
>> a.title = "Jimbo"
|
115
|
-
>> a.save
|
116
|
-
=> true
|
117
|
-
|
118
|
-
|
119
|
-
### Unique fields
|
120
|
-
|
121
|
-
The `:unique` argument in a field declaration gives `validates_uniqueness_of`:
|
122
|
-
|
123
|
-
>>
|
124
|
-
class Advert
|
125
|
-
fields do
|
126
|
-
title :string, :unique, limit: 255
|
127
|
-
end
|
128
|
-
end
|
129
|
-
>> a = Advert.new :title => "Jimbo", id: 3, body: "hello"
|
130
|
-
>> a.valid?
|
131
|
-
=> false
|
132
|
-
>> a.errors.full_messages
|
133
|
-
=> ["Title has already been taken"]
|
134
|
-
>> a.title = "Sambo"
|
135
|
-
>> a.save
|
136
|
-
=> true
|
data/test/generators.rdoctest
DELETED
@@ -1,62 +0,0 @@
|
|
1
|
-
doctest: prepare testapp environment
|
2
|
-
doctest_require: 'prepare_testapp'
|
3
|
-
|
4
|
-
doctest: generate declare_schema:model
|
5
|
-
>> begin; Rails::Generators.invoke 'declare_schema:model', %w(alpha/beta one:string two:integer); rescue => ex; $stderr.puts "#{ex.class}: #{ex}\n#{ex.backtrace.join("\n")}"; end
|
6
|
-
|
7
|
-
|
8
|
-
doctest: model file exists
|
9
|
-
>> File.exist? 'app/models/alpha/beta.rb'
|
10
|
-
=> true
|
11
|
-
|
12
|
-
doctest: model content matches
|
13
|
-
>> File.read 'app/models/alpha/beta.rb'
|
14
|
-
=> "class Alpha::Beta < #{Rails::VERSION::MAJOR > 4 ? 'ApplicationRecord' : 'ActiveRecord::Base'}\n\n fields do\n one :string, limit: 255\n two :integer\n end\n\nend\n"
|
15
|
-
|
16
|
-
doctest: module file exists
|
17
|
-
>> File.exist? 'app/models/alpha.rb'
|
18
|
-
=> true
|
19
|
-
|
20
|
-
doctest: module content matches
|
21
|
-
>> File.read 'app/models/alpha.rb'
|
22
|
-
=> "module Alpha\n def self.table_name_prefix\n 'alpha_'\n end\nend\n"
|
23
|
-
|
24
|
-
|
25
|
-
doctest: test file exists
|
26
|
-
>> File.exist? 'test/models/alpha/beta_test.rb'
|
27
|
-
=> true
|
28
|
-
|
29
|
-
doctest: test content matches
|
30
|
-
>> File.read 'test/models/alpha/beta_test.rb'
|
31
|
-
=>
|
32
|
-
require 'test_helper'
|
33
|
-
|
34
|
-
class Alpha::BetaTest < ActiveSupport::TestCase
|
35
|
-
# test "the truth" do
|
36
|
-
# assert true
|
37
|
-
# end
|
38
|
-
end
|
39
|
-
|
40
|
-
doctest: fixture file exists
|
41
|
-
>> File.exist? 'test/fixtures/alpha/beta.yml'
|
42
|
-
=> true
|
43
|
-
|
44
|
-
|
45
|
-
doctest: generate declare_schema:migration
|
46
|
-
>> puts "#{Rails.root}/app/models/alpha.rb"
|
47
|
-
>> require "#{Rails.root}/app/models/alpha.rb" if Rails::VERSION::MAJOR > 4
|
48
|
-
>> require "#{Rails.root}/app/models/alpha/beta.rb" if Rails::VERSION::MAJOR > 4
|
49
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
50
|
-
|
51
|
-
doctest: schema.rb file exists
|
52
|
-
>> system("ls -al db")
|
53
|
-
>> File.exist? 'db/schema.rb'
|
54
|
-
=> true
|
55
|
-
|
56
|
-
doctest: db file exists
|
57
|
-
>> File.exist?("db/development.sqlite3") || File.exist?("db/test.sqlite3")
|
58
|
-
=> true
|
59
|
-
|
60
|
-
doctest: Alpha::Beta class exists
|
61
|
-
>> Alpha::Beta
|
62
|
-
# will error if class doesn't exist
|
@@ -1,56 +0,0 @@
|
|
1
|
-
-*- indent-tabs-mode:nil; -*-
|
2
|
-
|
3
|
-
# DeclareSchema - Migration Generator
|
4
|
-
|
5
|
-
Our test requires to prepare the testapp:
|
6
|
-
{.hidden}
|
7
|
-
|
8
|
-
doctest_require: 'prepare_testapp'
|
9
|
-
|
10
|
-
{.hidden}
|
11
|
-
|
12
|
-
And requires also that you enter the right choice when prompted. OK we're ready to get going.
|
13
|
-
|
14
|
-
## Alternate Primary Keys
|
15
|
-
|
16
|
-
### create
|
17
|
-
doctest: create table with custom primary_key
|
18
|
-
>>
|
19
|
-
class Foo < ActiveRecord::Base
|
20
|
-
fields do
|
21
|
-
end
|
22
|
-
self.primary_key="foo_id"
|
23
|
-
end
|
24
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
25
|
-
>> Foo.primary_key
|
26
|
-
=> 'foo_id'
|
27
|
-
|
28
|
-
### migrate from
|
29
|
-
doctest: rename from custom primary_key
|
30
|
-
>>
|
31
|
-
class Foo < ActiveRecord::Base
|
32
|
-
self.primary_key="id"
|
33
|
-
end
|
34
|
-
puts "\n\e[45m Please enter 'id' (no quotes) at the next prompt \e[0m"
|
35
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
36
|
-
>> Foo.primary_key
|
37
|
-
=> 'id'
|
38
|
-
|
39
|
-
### migrate to
|
40
|
-
|
41
|
-
doctest: rename to custom primary_key
|
42
|
-
>>
|
43
|
-
class Foo < ActiveRecord::Base
|
44
|
-
self.primary_key="foo_id"
|
45
|
-
end
|
46
|
-
puts "\n\e[45m Please enter 'drop id' (no quotes) at the next prompt \e[0m"
|
47
|
-
>> Rails::Generators.invoke 'declare_schema:migration', %w(-n -m)
|
48
|
-
>> Foo.primary_key
|
49
|
-
=> 'foo_id'
|
50
|
-
|
51
|
-
### ensure it doesn't cause further migrations
|
52
|
-
|
53
|
-
doctest: check no further migrations
|
54
|
-
>> up, down = Generators::DeclareSchema::Migration::Migrator.run
|
55
|
-
>> up
|
56
|
-
=> ""
|