rswift 0.1.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 (84) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +56 -0
  7. data/Rakefile +6 -0
  8. data/bin/rswift +7 -0
  9. data/lib/ext/array.rb +12 -0
  10. data/lib/ext/group.rb +24 -0
  11. data/lib/ext/native_target.rb +54 -0
  12. data/lib/ext/project.rb +68 -0
  13. data/lib/rswift.rb +22 -0
  14. data/lib/rswift/attributes_configurator.rb +20 -0
  15. data/lib/rswift/build_settings_configurator.rb +26 -0
  16. data/lib/rswift/build_settings_provider.rb +84 -0
  17. data/lib/rswift/cli.rb +33 -0
  18. data/lib/rswift/configuration.rb +41 -0
  19. data/lib/rswift/constants.rb +106 -0
  20. data/lib/rswift/device_provider.rb +25 -0
  21. data/lib/rswift/files_references_manager.rb +17 -0
  22. data/lib/rswift/group_references_manager.rb +114 -0
  23. data/lib/rswift/project_configurator.rb +41 -0
  24. data/lib/rswift/scheme_configurator.rb +10 -0
  25. data/lib/rswift/target_configurator.rb +22 -0
  26. data/lib/rswift/template_manager.rb +28 -0
  27. data/lib/rswift/templates/app/ios/.gitignore.erb +22 -0
  28. data/lib/rswift/templates/app/ios/Gemfile.erb +4 -0
  29. data/lib/rswift/templates/app/ios/Podfile.erb +11 -0
  30. data/lib/rswift/templates/app/ios/Rakefile.erb +1 -0
  31. data/lib/rswift/templates/app/ios/app/AppDelegate.swift.erb +15 -0
  32. data/lib/rswift/templates/app/ios/app/Assets.xcassets/AppIcon.appiconset/Contents.json.erb +73 -0
  33. data/lib/rswift/templates/app/ios/app/Info.plist.erb +45 -0
  34. data/lib/rswift/templates/app/ios/app/LaunchScreen.storyboard.erb +28 -0
  35. data/lib/rswift/templates/app/ios/package.json.erb +14 -0
  36. data/lib/rswift/templates/app/ios/spec/AppDelegateSpec.swift.erb +24 -0
  37. data/lib/rswift/templates/app/ios/spec/Info.plist.erb +24 -0
  38. data/lib/rswift/templates/app/osx/.gitignore.erb +21 -0
  39. data/lib/rswift/templates/app/osx/Gemfile.erb +4 -0
  40. data/lib/rswift/templates/app/osx/Podfile.erb +7 -0
  41. data/lib/rswift/templates/app/osx/Rakefile.erb +1 -0
  42. data/lib/rswift/templates/app/osx/app/AppDelegate.swift.erb +30 -0
  43. data/lib/rswift/templates/app/osx/app/Assets.xcassets/AppIcon.appiconset/Contents.json.erb +58 -0
  44. data/lib/rswift/templates/app/osx/app/Info.plist.erb +30 -0
  45. data/lib/rswift/templates/app/osx/app/main.swift.erb +5 -0
  46. data/lib/rswift/templates/app/osx/spec/AppDelegateSpec.swift.erb +24 -0
  47. data/lib/rswift/templates/app/osx/spec/Info.plist.erb +24 -0
  48. data/lib/rswift/templates/app/tvos/.gitignore.erb +21 -0
  49. data/lib/rswift/templates/app/tvos/Gemfile.erb +4 -0
  50. data/lib/rswift/templates/app/tvos/Podfile.erb +11 -0
  51. data/lib/rswift/templates/app/tvos/Rakefile.erb +1 -0
  52. data/lib/rswift/templates/app/tvos/app/AppDelegate.swift.erb +15 -0
  53. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Content.imageset/Contents.json.erb +12 -0
  54. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Back.imagestacklayer/Contents.json.erb +6 -0
  55. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Contents.json.erb +17 -0
  56. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Content.imageset/Contents.json.erb +12 -0
  57. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Front.imagestacklayer/Contents.json.erb +6 -0
  58. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json.erb +12 -0
  59. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Large.imagestack/Middle.imagestacklayer/Contents.json.erb +6 -0
  60. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Content.imageset/Contents.json.erb +12 -0
  61. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Back.imagestacklayer/Contents.json.erb +6 -0
  62. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Contents.json.erb +17 -0
  63. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Content.imageset/Contents.json.erb +12 -0
  64. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Front.imagestacklayer/Contents.json.erb +6 -0
  65. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Content.imageset/Contents.json.erb +12 -0
  66. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/App Icon - Small.imagestack/Middle.imagestacklayer/Contents.json.erb +6 -0
  67. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/Contents.json.erb +26 -0
  68. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/AppIcon.brandassets/Top Shelf Image.imageset/Contents.json.erb +12 -0
  69. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/Contents.json.erb +6 -0
  70. data/lib/rswift/templates/app/tvos/app/Assets.xcassets/LaunchImage.launchimage/Contents.json.erb +15 -0
  71. data/lib/rswift/templates/app/tvos/app/Info.plist.erb +30 -0
  72. data/lib/rswift/templates/app/tvos/spec/AppDelegateSpec.swift.erb +24 -0
  73. data/lib/rswift/templates/app/tvos/spec/Info.plist.erb +24 -0
  74. data/lib/rswift/templates/app/watchos/wk_app/Assets.xcassets/AppIcon.appiconset/Contents.json.erb +55 -0
  75. data/lib/rswift/templates/app/watchos/wk_app/Info.plist.erb +35 -0
  76. data/lib/rswift/templates/app/watchos/wk_app/Interface.storyboard.erb +15 -0
  77. data/lib/rswift/templates/app/watchos/wk_ext/Assets.xcassets/README__ignoredByTemplate__ +1 -0
  78. data/lib/rswift/templates/app/watchos/wk_ext/ExtensionDelegate.swift.erb +4 -0
  79. data/lib/rswift/templates/app/watchos/wk_ext/Info.plist.erb +40 -0
  80. data/lib/rswift/templates/app/watchos/wk_ext/InterfaceController.swift.erb +6 -0
  81. data/lib/rswift/version.rb +3 -0
  82. data/lib/rswift/workspace_provider.rb +11 -0
  83. data/rswift.gemspec +26 -0
  84. metadata +227 -0
@@ -0,0 +1,33 @@
1
+ require 'thor'
2
+
3
+ module RSwift
4
+ class CLI < Thor
5
+ attr_accessor :template_manager
6
+ attr_accessor :project_configurator
7
+
8
+ def initialize(args = [], local_options = {}, config = {})
9
+ super
10
+ @template_manager = RSwift::TemplateManager.new
11
+ @project_configurator = RSwift::ProjectConfigurator.new
12
+ end
13
+
14
+ desc 'app NAME', 'create new empty application project'
15
+ option :template
16
+
17
+ def app(name)
18
+ template = options[:template]
19
+ template ||= 'ios'
20
+ template = template.to_sym
21
+ abort 'Available templates: ios (default), osx, tvos, watchos' unless RSwift::Constants::TEMPLATE_PROPERTIES.keys.include? template
22
+ abort 'Not implemented yet' if template == :watchos
23
+
24
+ @template_manager.create_files_for_template(name, template)
25
+
26
+ project_path = "#{name}/#{name}.xcodeproj"
27
+ project = Xcodeproj::Project.new project_path
28
+ @project_configurator.configure_project(project, template)
29
+ project.save
30
+ say_status :generate, project_path
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,41 @@
1
+ require 'yaml'
2
+
3
+ module RSwift
4
+ class Configuration
5
+
6
+ def initialize
7
+ yaml_file = '.rswift.yml'
8
+ @config = YAML.load_file(yaml_file) if File.exist?(yaml_file)
9
+ end
10
+
11
+ def app_scheme_name
12
+ @config['app_scheme_name'] if @config
13
+ end
14
+
15
+ def product_name
16
+ @config['product_name'] if @config
17
+ end
18
+
19
+ def debug_build_configuration
20
+ @config['debug_build_configuration'] if @config
21
+ end
22
+
23
+ def release_build_configuration
24
+ @config['release_build_configuration'] if @config
25
+ end
26
+
27
+ def debug_product_bundle_identifier
28
+ @config['debug_product_bundle_identifier'] if @config
29
+ end
30
+
31
+ def release_product_bundle_identifier
32
+ @config['release_product_bundle_identifier'] if @config
33
+ end
34
+
35
+ def group_name(target)
36
+ group_name = @config[RSwift::Constants::TARGET_PROPERTIES[target.product_type_uti][:configuration_key]] if @config
37
+ group_name ||= RSwift::Constants::TARGET_PROPERTIES[target.product_type_uti][:group_name]
38
+ group_name
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,106 @@
1
+ require 'xcodeproj'
2
+
3
+ module RSwift
4
+ module Constants
5
+
6
+ CONFIGURATION_PROPERTIES = {
7
+ debug: {
8
+ name: 'Debug'
9
+ }.freeze,
10
+ release: {
11
+ name: 'Release'
12
+ }
13
+ }.freeze
14
+
15
+ TEMPLATE_PROPERTIES = {
16
+ ios: {
17
+ target_types: [:application, :unit_test_bundle].freeze,
18
+ sdk: "iOS #{Xcodeproj::XcodebuildHelper.new.last_ios_sdk}"
19
+ }.freeze,
20
+ osx: {
21
+ target_types: [:application, :unit_test_bundle].freeze,
22
+ sdk: "OSX #{Xcodeproj::XcodebuildHelper.new.last_osx_sdk}"
23
+ }.freeze,
24
+ tvos: {
25
+ target_types: [:application, :unit_test_bundle].freeze,
26
+ sdk: "tvOS #{Xcodeproj::XcodebuildHelper.new.last_tvos_sdk}"
27
+ }.freeze,
28
+ watchos: {
29
+ target_types: [:watch2_app, :watch2_extension].freeze,
30
+ sdk: "watchOS #{Xcodeproj::XcodebuildHelper.new.last_watchos_sdk}"
31
+ }.freeze
32
+ }.freeze
33
+
34
+ TARGET_PROPERTIES = {
35
+ application: {
36
+ group_name: 'app',
37
+ suffix: '',
38
+ configuration_key: 'app_group'
39
+ }.freeze,
40
+ unit_test_bundle: {
41
+ group_name: 'spec',
42
+ suffix: 'Specs',
43
+ configuration_key: 'spec_group'
44
+ }.freeze,
45
+ watch2_app: {
46
+ group_name: 'wk_app',
47
+ suffix: ' WatchKit App',
48
+ configuration_key: 'wk_app_group'
49
+ }.freeze,
50
+ watch2_extension: {
51
+ group_name: 'wk_ext',
52
+ suffix: ' WatchKit Extension',
53
+ configuration_key: 'wk_ext_group'
54
+ }.freeze
55
+ }.freeze
56
+
57
+ COMPILE_SOURCES_EXTENSIONS = %w(
58
+ .s
59
+ .c
60
+ .exp
61
+ .cpp
62
+ .m
63
+ .swift
64
+ .metal
65
+ .xcdatamodeld
66
+ .xcdatamodel
67
+ .xcmappingmodel
68
+ ).freeze
69
+
70
+ RESOURCES_EXTENSIONS = %w(
71
+ .xcconfig
72
+ .xcassets
73
+ .plist
74
+ .sh
75
+ .scn
76
+ .scnp
77
+ .sks
78
+ .strings
79
+ .xib
80
+ .storyboard
81
+ .apns
82
+ .bundle
83
+ .aiff
84
+ .midi
85
+ .mp3
86
+ .wav
87
+ .au
88
+ .bmp
89
+ .gif
90
+ .jpg
91
+ .png
92
+ .tiff
93
+ .avi
94
+ .mpeg
95
+ .mov
96
+ .xml
97
+ .html
98
+ .css
99
+ .json
100
+ .md
101
+ .txt
102
+ .rtf
103
+ .pdf
104
+ ).freeze
105
+ end
106
+ end
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+
3
+ module RSwift
4
+ class DeviceProvider
5
+
6
+ def self.udid_for_device(device_name, template)
7
+ devices = devices_for_template(template)
8
+ devices[device_name].udid
9
+ end
10
+
11
+ private
12
+
13
+ def self.devices_for_template(template)
14
+ devices_json = `xcrun simctl list devices -j`
15
+ devices = JSON.parse(devices_json)['devices']
16
+ sdk = RSwift::Constants::TEMPLATE_PROPERTIES[template][:sdk]
17
+ devices_for_sdk = {}
18
+ devices[sdk].each do |device_hash|
19
+ device = OpenStruct.new(device_hash)
20
+ devices_for_sdk[device.name] = device
21
+ end
22
+ devices_for_sdk
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ module RSwift
2
+ class FilesReferencesManager
3
+
4
+ attr_accessor :group_references_manager
5
+
6
+ def initialize
7
+ @group_references_manager = RSwift::GroupReferencesManager.new(self)
8
+ end
9
+
10
+ def update_target_references(group, target)
11
+ @group_references_manager.update_files_references(group, target)
12
+ @group_references_manager.update_directory_references(group, target)
13
+ @group_references_manager.cleanup_invalid_references(group)
14
+ @group_references_manager.cleanup_build_files(target)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,114 @@
1
+ module RSwift
2
+ class GroupReferencesManager
3
+
4
+ attr_accessor :files_references_manager
5
+
6
+ def initialize(files_references_manager)
7
+ @files_references_manager = files_references_manager
8
+ end
9
+
10
+ def update_files_references(group, target)
11
+ compile_sources_extensions = RSwift::Constants::COMPILE_SOURCES_EXTENSIONS
12
+ resources_extensions = RSwift::Constants::RESOURCES_EXTENSIONS
13
+
14
+ entries = Dir.glob("#{group.real_path.to_path}/*")
15
+ file_entries = entries.select { |entry| File.file? entry }
16
+
17
+ file_entries.each do |entry|
18
+ basename = File.basename entry
19
+ extname = File.extname entry
20
+ file_reference = group.file_for_path(basename)
21
+ if basename == 'Info.plist' || extname == '.xcdatamodel'
22
+ next
23
+ elsif compile_sources_extensions.include? extname
24
+ target.source_build_phase.add_file_reference(file_reference, true)
25
+ elsif resources_extensions.include? extname
26
+ target.resources_build_phase.add_file_reference(file_reference, true)
27
+ end
28
+ end
29
+ end
30
+
31
+ def update_directory_references(group, target)
32
+ compile_sources_extensions = RSwift::Constants::COMPILE_SOURCES_EXTENSIONS
33
+ resources_extensions = RSwift::Constants::RESOURCES_EXTENSIONS
34
+
35
+ entries = Dir.glob("#{group.real_path.to_path}/*")
36
+ directory_entries = entries.select { |entry| !File.file? entry }
37
+
38
+ directory_entries.each do |entry|
39
+ basename = File.basename entry
40
+ extname = File.extname entry
41
+ if compile_sources_extensions.include? extname
42
+ if extname != '.xcdatamodeld'
43
+ file_reference = group.file_for_path(basename)
44
+ target.source_build_phase.add_file_reference(file_reference, true)
45
+ else
46
+ add_xcdatamodel(entry, group, target)
47
+ end
48
+ elsif resources_extensions.include? extname
49
+ file_reference = group.file_for_path(basename)
50
+ target.resources_build_phase.add_file_reference(file_reference, true)
51
+ else
52
+ group_reference = group.group_for_path(basename)
53
+ @files_references_manager.update_target_references(group_reference, target)
54
+ end
55
+ end
56
+ end
57
+
58
+ def cleanup_invalid_references(group)
59
+ compile_sources_extensions = RSwift::Constants::COMPILE_SOURCES_EXTENSIONS
60
+ resources_extensions = RSwift::Constants::RESOURCES_EXTENSIONS
61
+
62
+ invalid_files_refs = group.files
63
+ invalid_groups_refs = group.groups
64
+
65
+ entries = Dir.glob("#{group.real_path.to_path}/*")
66
+ entries.each do |entry|
67
+ basename = File.basename entry
68
+ extname = File.extname entry
69
+
70
+ if compile_sources_extensions.include? extname
71
+ file_ref = invalid_files_refs.find { |invalid_file_ref| invalid_file_ref.path == basename }
72
+ invalid_files_refs.delete(file_ref)
73
+ elsif resources_extensions.include? extname
74
+ file_ref = invalid_files_refs.find { |invalid_file_ref| invalid_file_ref.path == basename }
75
+ invalid_files_refs.delete(file_ref)
76
+ elsif File.file? entry
77
+ file_ref = invalid_files_refs.find { |invalid_file_ref| invalid_file_ref.path == basename }
78
+ invalid_files_refs.delete(file_ref)
79
+ else
80
+ group_ref = invalid_groups_refs.find { |invalid_group_ref| invalid_group_ref.path == basename }
81
+ invalid_groups_refs.delete(group_ref)
82
+ end
83
+ end
84
+
85
+ invalid_files_refs.each { |file_ref| file_ref.remove_from_project }
86
+ invalid_groups_refs.each do |group_ref|
87
+ group_ref.clear
88
+ group_ref.remove_from_project
89
+ end
90
+ end
91
+
92
+ def cleanup_build_files(target)
93
+ invalid_build_sources = target.source_build_phase.files.select { |file| file.file_ref == nil }
94
+ invalid_build_sources.each { |file| target.source_build_phase.remove_build_file(file) }
95
+
96
+ invalid_resources = target.resources_build_phase.files.select { |file| file.file_ref == nil }
97
+ invalid_resources.each { |file| target.resources_build_phase.remove_build_file(file) }
98
+ end
99
+
100
+ private
101
+
102
+ def add_xcdatamodel(entry, group, target)
103
+ basename = File.basename entry
104
+ extname = File.extname entry
105
+ version_group = group.version_groups.find { |version_group| version_group.path == basename }
106
+ if version_group == nil
107
+ file_reference = group.file_for_path(basename)
108
+ target.source_build_phase.add_file_reference(file_reference, true)
109
+ version_group = group.version_groups.find { |version_group| version_group.path == basename }
110
+ @files_references_manager.update_target_references(version_group, target)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,41 @@
1
+ require 'xcodeproj'
2
+
3
+ module RSwift
4
+ class ProjectConfigurator
5
+ attr_accessor :build_settings_configurator
6
+ attr_accessor :target_configurator
7
+ attr_accessor :attributes_configurator
8
+ attr_accessor :scheme_configurator
9
+
10
+ def initialize
11
+ @build_settings_configurator = RSwift::BuildSettingsConfigurator.new
12
+ @target_configurator = RSwift::TargetConfigurator.new
13
+ @attributes_configurator = RSwift::AttributesConfigurator.new
14
+ @scheme_configurator = RSwift::SchemeConfigurator.new
15
+ end
16
+
17
+ def configure_project(project, template)
18
+ setup_targets(project, template)
19
+ setup_schemes project
20
+ @build_settings_configurator.configure_project_settings(project, template)
21
+ @attributes_configurator.configure_project_attributes project
22
+ end
23
+
24
+ private
25
+
26
+ def setup_targets(project, template)
27
+ target_types = RSwift::Constants::TEMPLATE_PROPERTIES[template][:target_types]
28
+ target_types.each do |target_type|
29
+ target_properties = RSwift::Constants::TARGET_PROPERTIES[target_type]
30
+ target = project.new_target(target_type, "#{project.name}#{target_properties[:suffix]}", template)
31
+ @target_configurator.configure_target(project, target, template)
32
+ end
33
+ @target_configurator.configure_targets_dependencies project
34
+ end
35
+
36
+ def setup_schemes(project)
37
+ scheme = Xcodeproj::XCScheme.new
38
+ @scheme_configurator.configure_app_scheme(project, scheme)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,10 @@
1
+ module RSwift
2
+ class SchemeConfigurator
3
+ def configure_app_scheme(project, scheme)
4
+ scheme.add_build_target(project.app_target)
5
+ scheme.set_launch_target(project.app_target)
6
+ scheme.add_test_target(project.spec_target)
7
+ scheme.save_as(project.path, project.name)
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,22 @@
1
+ module RSwift
2
+ class TargetConfigurator
3
+
4
+ attr_accessor :build_settings_configurator
5
+ attr_accessor :files_references_manager
6
+
7
+ def initialize
8
+ @build_settings_configurator = RSwift::BuildSettingsConfigurator.new
9
+ @files_references_manager = RSwift::FilesReferencesManager.new
10
+ end
11
+
12
+ def configure_target(project, target, template)
13
+ @build_settings_configurator.configure_target_settings(project, target, template)
14
+ group = project.new_group(target.group_name, target.group_name)
15
+ @files_references_manager.update_target_references(group, target)
16
+ end
17
+
18
+ def configure_targets_dependencies(project)
19
+ project.spec_target.add_dependency(project.app_target)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ module RSwift
2
+ class TemplateManager
3
+ include Thor::Base
4
+ include Thor::Actions
5
+
6
+ attr_reader :name
7
+
8
+ def self.source_root
9
+ File.dirname(__FILE__)
10
+ end
11
+
12
+ def create_files_for_template(name, template)
13
+ @name = name
14
+ current_directory_path = File.dirname(__FILE__)
15
+ template_directory = File.join(current_directory_path, 'templates/app', template.to_s)
16
+ Dir.glob("#{template_directory}/**/*.erb", File::FNM_DOTMATCH).each do |template_path|
17
+ relative_template_path = template_path.sub(current_directory_path + '/', '')
18
+
19
+ relative_erb_file_path = template_path.sub(template_directory, '')
20
+ file_name = File.basename(relative_erb_file_path, '.erb')
21
+ relative_directory_path = File.dirname(relative_erb_file_path)
22
+ relative_file_path = File.join(name, relative_directory_path, file_name)
23
+
24
+ template relative_template_path, relative_file_path
25
+ end
26
+ end
27
+ end
28
+ end