dm-svn 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.
- data/.gitignore +23 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/dm-svn.gemspec +85 -0
- data/lib/dm-svn.rb +13 -0
- data/lib/dm-svn/config.rb +38 -0
- data/lib/dm-svn/model.rb +12 -0
- data/lib/dm-svn/svn.rb +144 -0
- data/lib/dm-svn/svn/categorized.rb +113 -0
- data/lib/dm-svn/svn/changeset.rb +119 -0
- data/lib/dm-svn/svn/node.rb +128 -0
- data/lib/dm-svn/svn/sync.rb +85 -0
- data/spec/dm-svn/config_spec.rb +51 -0
- data/spec/dm-svn/database.yml +16 -0
- data/spec/dm-svn/fixtures/articles_comments.rb +95 -0
- data/spec/dm-svn/mock_models.rb +53 -0
- data/spec/dm-svn/model_spec.rb +5 -0
- data/spec/dm-svn/spec_helper.rb +50 -0
- data/spec/dm-svn/svn/categorized_spec.rb +138 -0
- data/spec/dm-svn/svn/changeset_spec.rb +42 -0
- data/spec/dm-svn/svn/node_spec.rb +125 -0
- data/spec/dm-svn/svn/sync_spec.rb +111 -0
- data/spec/dm-svn/svn_spec.rb +213 -0
- data/spec/spec.opts +0 -0
- data/spec/spec_helper.rb +23 -0
- metadata +132 -0
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
.DS_Store
|
2
|
+
log/*
|
3
|
+
tmp/*
|
4
|
+
TAGS
|
5
|
+
*~
|
6
|
+
.#*
|
7
|
+
schema/schema.rb
|
8
|
+
schema/*_structure.sql
|
9
|
+
schema/*.sqlite3
|
10
|
+
schema/*.sqlite
|
11
|
+
schema/*.db
|
12
|
+
*.sqlite
|
13
|
+
*.sqlite3
|
14
|
+
*.db
|
15
|
+
src/*
|
16
|
+
.hgignore
|
17
|
+
.hg/*
|
18
|
+
.svn/*
|
19
|
+
.project
|
20
|
+
.loadpath
|
21
|
+
lib/wistle/tmp/*
|
22
|
+
config/recaptcha.yml
|
23
|
+
*qt_temp*
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "dm-svn"
|
8
|
+
gem.summary = %Q{Sync content from a Subversion repository to a DataMapper model}
|
9
|
+
gem.description = %Q{dm-svn allows you to store data in a Subversion
|
10
|
+
repository, then sync that data to a DataMapper model (for example, to a
|
11
|
+
relational database. Essentially, it allows you app quicker access to the
|
12
|
+
Subversion data.}
|
13
|
+
gem.email = "jmorgan@morgancreative.net"
|
14
|
+
gem.homepage = "http://github.com/jm81/dm-svn"
|
15
|
+
gem.authors = ["Jared Morgan"]
|
16
|
+
gem.add_dependency('dm-core', '>= 0.10.0')
|
17
|
+
gem.add_dependency('dm-aggregates', '>= 0.10.0')
|
18
|
+
gem.add_dependency('dm-validations', '>= 0.10.0')
|
19
|
+
gem.add_dependency('jm81-svn-fixture', '>= 0.1.1')
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'spec/rake/spectask'
|
28
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
34
|
+
spec.libs << 'lib' << 'spec'
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
if File.exist?('VERSION.yml')
|
45
|
+
config = YAML.load(File.read('VERSION.yml'))
|
46
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
47
|
+
else
|
48
|
+
version = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "dm-svn #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
56
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/dm-svn.gemspec
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{dm-svn}
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jared Morgan"]
|
12
|
+
s.date = %q{2009-10-11}
|
13
|
+
s.description = %q{dm-svn allows you to store data in a Subversion
|
14
|
+
repository, then sync that data to a DataMapper model (for example, to a
|
15
|
+
relational database. Essentially, it allows you app quicker access to the
|
16
|
+
Subversion data.}
|
17
|
+
s.email = %q{jmorgan@morgancreative.net}
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION",
|
22
|
+
"dm-svn.gemspec",
|
23
|
+
"lib/dm-svn.rb",
|
24
|
+
"lib/dm-svn/config.rb",
|
25
|
+
"lib/dm-svn/model.rb",
|
26
|
+
"lib/dm-svn/svn.rb",
|
27
|
+
"lib/dm-svn/svn/categorized.rb",
|
28
|
+
"lib/dm-svn/svn/changeset.rb",
|
29
|
+
"lib/dm-svn/svn/node.rb",
|
30
|
+
"lib/dm-svn/svn/sync.rb",
|
31
|
+
"spec/dm-svn/config_spec.rb",
|
32
|
+
"spec/dm-svn/database.yml",
|
33
|
+
"spec/dm-svn/fixtures/articles_comments.rb",
|
34
|
+
"spec/dm-svn/mock_models.rb",
|
35
|
+
"spec/dm-svn/model_spec.rb",
|
36
|
+
"spec/dm-svn/spec_helper.rb",
|
37
|
+
"spec/dm-svn/svn/categorized_spec.rb",
|
38
|
+
"spec/dm-svn/svn/changeset_spec.rb",
|
39
|
+
"spec/dm-svn/svn/node_spec.rb",
|
40
|
+
"spec/dm-svn/svn/sync_spec.rb",
|
41
|
+
"spec/dm-svn/svn_spec.rb",
|
42
|
+
"spec/spec.opts",
|
43
|
+
"spec/spec_helper.rb"
|
44
|
+
]
|
45
|
+
s.homepage = %q{http://github.com/jm81/dm-svn}
|
46
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
47
|
+
s.require_paths = ["lib"]
|
48
|
+
s.rubygems_version = %q{1.3.5}
|
49
|
+
s.summary = %q{Sync content from a Subversion repository to a DataMapper model}
|
50
|
+
s.test_files = [
|
51
|
+
"spec/dm-svn/config_spec.rb",
|
52
|
+
"spec/dm-svn/fixtures/articles_comments.rb",
|
53
|
+
"spec/dm-svn/mock_models.rb",
|
54
|
+
"spec/dm-svn/model_spec.rb",
|
55
|
+
"spec/dm-svn/spec_helper.rb",
|
56
|
+
"spec/dm-svn/svn/categorized_spec.rb",
|
57
|
+
"spec/dm-svn/svn/changeset_spec.rb",
|
58
|
+
"spec/dm-svn/svn/node_spec.rb",
|
59
|
+
"spec/dm-svn/svn/sync_spec.rb",
|
60
|
+
"spec/dm-svn/svn_spec.rb",
|
61
|
+
"spec/spec_helper.rb"
|
62
|
+
]
|
63
|
+
|
64
|
+
if s.respond_to? :specification_version then
|
65
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
66
|
+
s.specification_version = 3
|
67
|
+
|
68
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
69
|
+
s.add_runtime_dependency(%q<dm-core>, [">= 0.10.0"])
|
70
|
+
s.add_runtime_dependency(%q<dm-aggregates>, [">= 0.10.0"])
|
71
|
+
s.add_runtime_dependency(%q<dm-validations>, [">= 0.10.0"])
|
72
|
+
s.add_runtime_dependency(%q<jm81-svn-fixture>, [">= 0.1.1"])
|
73
|
+
else
|
74
|
+
s.add_dependency(%q<dm-core>, [">= 0.10.0"])
|
75
|
+
s.add_dependency(%q<dm-aggregates>, [">= 0.10.0"])
|
76
|
+
s.add_dependency(%q<dm-validations>, [">= 0.10.0"])
|
77
|
+
s.add_dependency(%q<jm81-svn-fixture>, [">= 0.1.1"])
|
78
|
+
end
|
79
|
+
else
|
80
|
+
s.add_dependency(%q<dm-core>, [">= 0.10.0"])
|
81
|
+
s.add_dependency(%q<dm-aggregates>, [">= 0.10.0"])
|
82
|
+
s.add_dependency(%q<dm-validations>, [">= 0.10.0"])
|
83
|
+
s.add_dependency(%q<jm81-svn-fixture>, [">= 0.1.1"])
|
84
|
+
end
|
85
|
+
end
|
data/lib/dm-svn.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'dm-core'
|
3
|
+
require 'dm-aggregates' # Only needed by specs, but this seems the easiest place to require.
|
4
|
+
require 'dm-validations'
|
5
|
+
require 'svn/client'
|
6
|
+
|
7
|
+
module DmSvn
|
8
|
+
VERSION = '0.2.0'
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'dm-svn/config'
|
12
|
+
require 'dm-svn/svn'
|
13
|
+
require 'dm-svn/model'
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module DmSvn
|
2
|
+
class Config
|
3
|
+
OPTS = [:uri, :username, :password,
|
4
|
+
:body_property, :property_prefix, :extension]
|
5
|
+
|
6
|
+
attr_accessor *OPTS
|
7
|
+
attr_accessor :path_from_root # Used by Sync.
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
# Set defaults
|
11
|
+
@body_property = 'body'
|
12
|
+
@property_prefix = 'ws:'
|
13
|
+
@extension = 'txt'
|
14
|
+
|
15
|
+
# Try to set variables from database.yml.
|
16
|
+
# The location of database.yml should, I suppose, be configurable.
|
17
|
+
# Oh, well.
|
18
|
+
if Object.const_defined?("RAILS_ROOT")
|
19
|
+
f = "#{RAILS_ROOT}/config/database.yml"
|
20
|
+
env = Kernel.const_defined?("RAILS_ENV") ? RAILS_ENV : "development"
|
21
|
+
elsif Object.const_defined?("Merb")
|
22
|
+
f = "#{Merb.root}/config/database.yml"
|
23
|
+
env = Merb.env.to_sym || :development
|
24
|
+
end
|
25
|
+
|
26
|
+
if f
|
27
|
+
config = YAML.load(IO.read(f))[env]
|
28
|
+
OPTS.each do |field|
|
29
|
+
config_field = config["svn_#{field}"] || config["svn_#{field}".to_sym]
|
30
|
+
if config_field
|
31
|
+
instance_variable_set("@#{field}", config_field)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/dm-svn/model.rb
ADDED
data/lib/dm-svn/svn.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
module DmSvn
|
2
|
+
module Svn
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def included(klass) # Set a few 'magic' properties
|
6
|
+
klass.extend(ClassMethods)
|
7
|
+
|
8
|
+
# svn_name could be just a name, or a full path, always excluding
|
9
|
+
# extension. If directories are stored in a model (not yet supported),
|
10
|
+
# it contains the full path (from the config.uri).
|
11
|
+
klass.property :svn_name, DataMapper::Types::Text, :lazy => false
|
12
|
+
klass.property :svn_created_at, DateTime
|
13
|
+
klass.property :svn_updated_at, DateTime
|
14
|
+
klass.property :svn_created_rev, String
|
15
|
+
klass.property :svn_updated_rev, String
|
16
|
+
klass.property :svn_created_by, String
|
17
|
+
klass.property :svn_updated_by, String
|
18
|
+
|
19
|
+
# On create, set svn_created_* attrs based on svn_updated_* attrs
|
20
|
+
klass.before :create do
|
21
|
+
attribute_set(:svn_created_at, svn_updated_at)
|
22
|
+
attribute_set(:svn_created_rev, svn_updated_rev)
|
23
|
+
attribute_set(:svn_created_by, svn_updated_by)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# +name+ could be reasonably used for another property, but may normally
|
29
|
+
# be assumed to be the 'svn_name'.
|
30
|
+
def name
|
31
|
+
@svn_name
|
32
|
+
end
|
33
|
+
|
34
|
+
# The path from the svn root for the model. For the moment, just an alias
|
35
|
+
# of +svn_name+.
|
36
|
+
def path
|
37
|
+
@svn_name
|
38
|
+
end
|
39
|
+
|
40
|
+
# Set the path. This may be responsible for moving the record to a different
|
41
|
+
# parent, etc.
|
42
|
+
def path=(value)
|
43
|
+
attribute_set(:svn_name, value)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Move to a different path and save
|
47
|
+
def move_to(new_path)
|
48
|
+
self.path = new_path
|
49
|
+
self.save
|
50
|
+
end
|
51
|
+
|
52
|
+
# Update properties (body and other properties) from a DmSvn::Svn::Node
|
53
|
+
# or similar (expects #body as a String and #properties as a Hash).
|
54
|
+
# This method calls #save.
|
55
|
+
def update_from_svn(node)
|
56
|
+
attribute_set(self.class.config.body_property, node.body) if node.body
|
57
|
+
self.path = node.short_path
|
58
|
+
|
59
|
+
node.properties.each do | attr, value |
|
60
|
+
if self.respond_to?("#{attr}=")
|
61
|
+
self.__send__("#{attr}=", value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if !valid?
|
66
|
+
puts "Invalid #{node.short_path} at revision #{node.revision}"
|
67
|
+
puts " - " + errors.full_messages.join(".\n - ")
|
68
|
+
end
|
69
|
+
|
70
|
+
save
|
71
|
+
end
|
72
|
+
|
73
|
+
module ClassMethods
|
74
|
+
def config
|
75
|
+
@config ||= Config.new
|
76
|
+
end
|
77
|
+
|
78
|
+
# Override belongs_to to add +:svn+ option. If :svn => true is
|
79
|
+
# included in the options, SvnSync will also sync the +belongs_to+ model.
|
80
|
+
# For example, <code>belongs_to :category, :svn => true</code>, means
|
81
|
+
# that the Category model will also be updated by SvnSync, and be based on
|
82
|
+
# folders. Folders can have svn properties set, and/or a meta.yml file
|
83
|
+
# with properties.
|
84
|
+
# def belongs_to(name, options={})
|
85
|
+
#
|
86
|
+
# end
|
87
|
+
|
88
|
+
# Override DataMapper's +property+ class method to accept as an option
|
89
|
+
# +body_property+. Setting this option tells DmSvn::Svn that this field
|
90
|
+
# will store the contents of the repository file.
|
91
|
+
def property(name, type, options = {})
|
92
|
+
if options.delete(:body_property)
|
93
|
+
config.body_property = name.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
super(name, type, options)
|
97
|
+
end
|
98
|
+
|
99
|
+
# DataMapper uses +repository+, so prepend "svn_"
|
100
|
+
def svn_repository
|
101
|
+
return @svn_repository if @svn_repository
|
102
|
+
|
103
|
+
@svn_repository = DmSvn::Model.first(:name => self.name)
|
104
|
+
@svn_repository ||= DmSvn::Model.create(:name => self.name, :revision => 0)
|
105
|
+
@svn_repository.config = config
|
106
|
+
@svn_repository
|
107
|
+
end
|
108
|
+
|
109
|
+
def sync
|
110
|
+
DmSvn::Svn::Sync.new(svn_repository).run
|
111
|
+
end
|
112
|
+
|
113
|
+
# Override normal get behavior to try to get based on path if the argument
|
114
|
+
# is a String. Extra args are ignored by default.
|
115
|
+
def get(path_or_id, *args)
|
116
|
+
if path_or_id.is_a?(String)
|
117
|
+
get_by_path(path_or_id)
|
118
|
+
else
|
119
|
+
super
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Try to get by path. If not, create a new record and so set its path.
|
124
|
+
def get_or_create(path)
|
125
|
+
i = get_by_path(path)
|
126
|
+
return i if i
|
127
|
+
|
128
|
+
i = create
|
129
|
+
i.path = path
|
130
|
+
i.save
|
131
|
+
return i
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_by_path(path)
|
135
|
+
first(:svn_name => path)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
%w{sync changeset node categorized}.each do |f|
|
143
|
+
require "dm-svn/svn/#{f}"
|
144
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module DmSvn
|
2
|
+
module Svn
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
|
6
|
+
# Override belongs_to to add a :dm-svn option if :dm-svn => true, include
|
7
|
+
# Categorized and set up @svn_category and @svn_category_model instance
|
8
|
+
# methods.
|
9
|
+
def belongs_to(what, options = {})
|
10
|
+
svn = options.delete(:svn)
|
11
|
+
if svn
|
12
|
+
@svn_category = what
|
13
|
+
@svn_category_model = options[:class_name] || what.to_s.camel_case
|
14
|
+
include(DmSvn::Svn::Categorized)
|
15
|
+
end
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
# Method name for accessing the parent instance.
|
21
|
+
def svn_category
|
22
|
+
@svn_category
|
23
|
+
end
|
24
|
+
|
25
|
+
# Name of the parent model class (as a String)
|
26
|
+
def svn_category_model
|
27
|
+
@svn_category_model
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
# This module is including when belongs_to is called with :dm-svn => true.
|
33
|
+
# It overrides #path and #path= to take into account categories (folders in
|
34
|
+
# the Subversion repository). It also overrides .get to accept get_parent
|
35
|
+
# argument.
|
36
|
+
module Categorized
|
37
|
+
class << self
|
38
|
+
def included(klass)
|
39
|
+
klass.extend(ClassMethods)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# The path from the svn root for the model. Includes any folders.
|
44
|
+
def path
|
45
|
+
cat = self.send(self.class.svn_category)
|
46
|
+
|
47
|
+
if cat && !cat.path.blank?
|
48
|
+
return cat.path + "/" + @svn_name
|
49
|
+
end
|
50
|
+
|
51
|
+
return @svn_name
|
52
|
+
end
|
53
|
+
|
54
|
+
# Set the path. This is responsible for moving the record to a different
|
55
|
+
# parent, etc.
|
56
|
+
def path=(value)
|
57
|
+
value = value[1..-1] while value[0..0] == "/"
|
58
|
+
ary = value.split("/")
|
59
|
+
immediate = ary.pop
|
60
|
+
parent = ary.join("/")
|
61
|
+
|
62
|
+
if parent.blank?
|
63
|
+
self.send("#{self.class.svn_category}=", nil)
|
64
|
+
else
|
65
|
+
category_model = Object.const_get(self.class.svn_category_model)
|
66
|
+
category = category_model.get_or_create(parent)
|
67
|
+
self.send("#{self.class.svn_category}=", category)
|
68
|
+
end
|
69
|
+
|
70
|
+
attribute_set(:svn_name, immediate)
|
71
|
+
end
|
72
|
+
|
73
|
+
module ClassMethods
|
74
|
+
|
75
|
+
# Get by path, which gets parent (possibly recursively) first.
|
76
|
+
def get_by_path(value)
|
77
|
+
value = value[1..-1] while value[0..0] == "/"
|
78
|
+
ary = value.split("/")
|
79
|
+
immediate = ary.pop
|
80
|
+
parent = ary.join("/")
|
81
|
+
|
82
|
+
if parent.blank?
|
83
|
+
first(:svn_name => immediate, "#{self.svn_category}_id".to_sym => nil)
|
84
|
+
else
|
85
|
+
category_model = Object.const_get(self.svn_category_model)
|
86
|
+
category = category_model.get_by_path(parent)
|
87
|
+
return nil if category.nil?
|
88
|
+
first(:svn_name => immediate, "#{self.svn_category}_id".to_sym => category.id)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Add get_parent argument. If true and a path is not found for the
|
93
|
+
# model, try to find path in parent model (if any).
|
94
|
+
def get(path_or_id, get_parent = false)
|
95
|
+
if path_or_id.is_a?(String)
|
96
|
+
i = get_by_path(path_or_id)
|
97
|
+
if i || !get_parent
|
98
|
+
return i
|
99
|
+
else # if get_parent
|
100
|
+
category_model = Object.const_get(@svn_category_model)
|
101
|
+
return nil if category_model == self.class
|
102
|
+
category_model.get_by_path(path_or_id)
|
103
|
+
end
|
104
|
+
else
|
105
|
+
super(path_or_id)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|