vipergeng 0.2.7

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +142 -0
  4. data/bin/vipergeng +9 -0
  5. data/lib/templates/default/objc/DataManager/API/VIPERAPIDataManager.h +12 -0
  6. data/lib/templates/default/objc/DataManager/API/VIPERAPIDataManager.m +10 -0
  7. data/lib/templates/default/objc/DataManager/Local/VIPERLocalDataManager.h +12 -0
  8. data/lib/templates/default/objc/DataManager/Local/VIPERLocalDataManager.m +10 -0
  9. data/lib/templates/default/objc/Interactor/VIPERInteractor.h +16 -0
  10. data/lib/templates/default/objc/Interactor/VIPERInteractor.m +10 -0
  11. data/lib/templates/default/objc/Interactor/VIPERItem.h +10 -0
  12. data/lib/templates/default/objc/Interactor/VIPERItem.m +10 -0
  13. data/lib/templates/default/objc/Presenter/VIPERPresenter.h +17 -0
  14. data/lib/templates/default/objc/Presenter/VIPERPresenter.m +11 -0
  15. data/lib/templates/default/objc/Protocols/VIPERProtocols.h +80 -0
  16. data/lib/templates/default/objc/View/VIPERView.h +13 -0
  17. data/lib/templates/default/objc/View/VIPERView.m +32 -0
  18. data/lib/templates/default/objc/WireFrame/VIPERWireFrame.h +18 -0
  19. data/lib/templates/default/objc/WireFrame/VIPERWireFrame.m +32 -0
  20. data/lib/templates/default/swift/DataManager/API/VIPERAPIDataManager.swift +11 -0
  21. data/lib/templates/default/swift/DataManager/Local/VIPERLocalDataManager.swift +11 -0
  22. data/lib/templates/default/swift/Interactor/VIPERInteractor.swift +15 -0
  23. data/lib/templates/default/swift/Interactor/VIPERItem.swift +13 -0
  24. data/lib/templates/default/swift/Presenter/VIPERPresenter.swift +15 -0
  25. data/lib/templates/default/swift/Protocols/VIPERProtocols.swift +70 -0
  26. data/lib/templates/default/swift/View/VIPERView.swift +12 -0
  27. data/lib/templates/default/swift/WireFrame/VIPERWireFrame.swift +29 -0
  28. data/lib/templates/default/viperspec.yml +4 -0
  29. data/lib/templates/naja/swift/Interactor/VIPERInteractor.swift +17 -0
  30. data/lib/templates/naja/swift/Presenter/VIPERPresenter.swift +21 -0
  31. data/lib/templates/naja/swift/Protocols/VIPERProtocols.swift +28 -0
  32. data/lib/templates/naja/swift/Views/VIPERView.swift +24 -0
  33. data/lib/templates/naja/swift/WireFrame/VIPERWireFrame.swift +41 -0
  34. data/lib/templates/naja/viperspec.yml +4 -0
  35. data/lib/vipergen/dirutils.rb +17 -0
  36. data/lib/vipergen/filemanager.rb +51 -0
  37. data/lib/vipergen/generator.rb +66 -0
  38. data/lib/vipergen/templatemanager.rb +53 -0
  39. data/lib/vipergen/version.rb +4 -0
  40. data/lib/vipergen/viperthor.rb +21 -0
  41. data/lib/vipergen.rb +6 -0
  42. data/spec/spec_helper.rb +4 -0
  43. data/spec/vipergen/vipergen_spec.rb +135 -0
  44. metadata +161 -0
@@ -0,0 +1,29 @@
1
+ //
2
+ // Created by AUTHOR
3
+ // Copyright (c) YEAR AUTHOR. All rights reserved.
4
+ //
5
+
6
+ import Foundation
7
+
8
+ class VIPERWireFrame: VIPERWireFrameProtocol
9
+ {
10
+ static func presentVIPERModule(fromView view: AnyObject)
11
+ {
12
+ // Generating module components
13
+ var view: VIPERViewProtocol = VIPERView()
14
+ var presenter: protocol<VIPERPresenterProtocol, VIPERInteractorOutputProtocol> = VIPERPresenter()
15
+ var interactor: VIPERInteractorInputProtocol = VIPERInteractor()
16
+ var APIDataManager: VIPERAPIDataManagerInputProtocol = VIPERAPIDataManager()
17
+ var localDataManager: VIPERLocalDataManagerInputProtocol = VIPERLocalDataManager()
18
+ var wireFrame: VIPERWireFrameProtocol = VIPERWireFrame()
19
+
20
+ // Connecting
21
+ view.presenter = presenter
22
+ presenter.view = view
23
+ presenter.wireFrame = wireFrame
24
+ presenter.interactor = interactor
25
+ interactor.presenter = presenter
26
+ interactor.APIDataManager = APIDataManager
27
+ interactor.localDatamanager = localDataManager
28
+ }
29
+ }
@@ -0,0 +1,4 @@
1
+ author: pepi
2
+ author_email: pepibumur@gmail.com
3
+ template_description: Default template with the simplest structure using VIPER
4
+ updated_at: 2014-08-24
@@ -0,0 +1,17 @@
1
+ //
2
+ // VIPERInteractor.swift
3
+ //
4
+ // Created by AUTHOR.
5
+ // Copyright © YEAR COMPANY. All rights reserved.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ class VIPERInteractor: VIPERInteractorInputProtocol {
11
+
12
+ // MARK: Properties
13
+
14
+ weak var presenter: VIPERInteractorOutputProtocol?
15
+
16
+ // MARK: VIPERInteractorInputProtocol
17
+ }
@@ -0,0 +1,21 @@
1
+ //
2
+ // VIPERPresenter.swift
3
+ //
4
+ // Created by AUTHOR.
5
+ // Copyright © YEAR COMPANY. All rights reserved.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ class VIPERPresenter: VIPERPresenterProtocol, VIPERInteractorOutputProtocol {
11
+
12
+ // MARK: Properties
13
+
14
+ weak var view: VIPERViewProtocol?
15
+ var interactor: VIPERInteractorInputProtocol?
16
+ var wireFrame: VIPERWireFrameProtocol?
17
+
18
+ // MARK: VIPERPresenterProtocol
19
+
20
+ // MARK: VIPERInteractorOutputProtocol
21
+ }
@@ -0,0 +1,28 @@
1
+ //
2
+ // VIPERProtocols.swift
3
+ //
4
+ // Created by AUTHOR.
5
+ // Copyright © YEAR COMPANY. All rights reserved.
6
+ //
7
+
8
+ import Foundation
9
+
10
+ protocol VIPERViewProtocol: class {
11
+
12
+ }
13
+
14
+ protocol VIPERPresenterProtocol: class {
15
+
16
+ }
17
+
18
+ protocol VIPERInteractorInputProtocol {
19
+
20
+ }
21
+
22
+ protocol VIPERInteractorOutputProtocol: class {
23
+
24
+ }
25
+
26
+ protocol VIPERWireFrameProtocol {
27
+
28
+ }
@@ -0,0 +1,24 @@
1
+ //
2
+ // VIPERView.swift
3
+ //
4
+ // Created by AUTHOR.
5
+ // Copyright © YEAR COMPANY. All rights reserved.
6
+ //
7
+
8
+ import Foundation
9
+ import UIKit
10
+
11
+ class VIPERView: UIViewController, VIPERViewProtocol {
12
+
13
+ // MARK: Properties
14
+
15
+ var presenter: VIPERPresenterProtocol?
16
+
17
+ // MARK: UIViewController
18
+
19
+ override func viewDidLoad() {
20
+ super.viewDidLoad()
21
+ }
22
+
23
+ // MARK: VIPERViewProtocol
24
+ }
@@ -0,0 +1,41 @@
1
+ //
2
+ // VIPERWireFrame.swift
3
+ //
4
+ // Created by AUTHOR.
5
+ // Copyright © YEAR COMPANY. All rights reserved.
6
+ //
7
+
8
+ import UIKit
9
+
10
+ public class VIPERWireFrame: VIPERWireFrameProtocol {
11
+
12
+ // MARK: Properties
13
+
14
+ weak var navigationController: UINavigationController?
15
+
16
+ // MARK: Factory
17
+
18
+ public static func presentVIPERFromViewController(source: UIViewController) {
19
+ let presenter = VIPERPresenter()
20
+ let view = VIPERView()
21
+ /*
22
+ If using UIStoryboard use:
23
+ let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
24
+ view = storyboard.instantiateViewControllerWithIdentifier("VIPERView") as! VIPERView
25
+ */
26
+ let interactor = VIPERInteractor()
27
+ let wireFrame = VIPERWireFrame()
28
+
29
+ view.presenter = presenter
30
+ interactor.presenter = presenter
31
+ presenter.view = view
32
+ presenter.interactor = interactor
33
+ presenter.wireFrame = wireFrame
34
+
35
+ let navigationController = UINavigationController(rootViewController: view)
36
+ wireFrame.navigationController = navigationController
37
+ source.presentViewController(navigationController, animated: true, completion: nil)
38
+ }
39
+
40
+ // MARK: VIPERWireFrameProtocol
41
+ }
@@ -0,0 +1,4 @@
1
+ author: Raphael Oliveira
2
+ author_email: raphaelfpoliveira@gmail.com
3
+ template_description: Yet another Swift VIPER template
4
+ updated_at: 2015-12-15
@@ -0,0 +1,17 @@
1
+ module Vipergen
2
+ class DirUtils
3
+ # Return a directory with the project libraries.
4
+ def self.gem_libdir
5
+ t = ["#{File.dirname(File.expand_path($0))}/../lib/#{Vipergen::NAME}",
6
+ "#{Gem.dir}/gems/#{Vipergen::NAME}-#{Vipergen::VERSION}/lib/#{Vipergen::NAME}"]
7
+ t.each {|i| return i if File.readable?(i) }
8
+ raise "both paths are invalid: #{t}"
9
+ end
10
+
11
+ # Returns the directories inside a given one
12
+ def self.directories_in(directory)
13
+ expanded_dir = File.expand_path(directory)
14
+ return Dir.glob(File.join(expanded_dir,'*')).select {|f| File.directory? f}
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,51 @@
1
+ module Vipergen
2
+ # File manager class
3
+ class FileManager
4
+
5
+ # Returns if the template is valid by the VIPER generator
6
+ def self.is_template_valid(template)
7
+ return Vipergen::TemplateManager.templates.include? template
8
+ end
9
+
10
+ # Returns if the language is valid by the VIPER generator
11
+ def self.is_language_valid(language)
12
+ return (Vipergen::Generator::LANGUAGES).include? language
13
+ end
14
+
15
+ # Return the path if valid template and language
16
+ # @return String with valid path
17
+ def self.path_from(template, language)
18
+ return nil if !is_language_valid(language) || !is_template_valid(template)
19
+ return File.join(Vipergen::TemplateManager.templates_dir, template, language)
20
+ end
21
+
22
+ # Returns an array with files in a given path
23
+ # @return Array with the files in a given path
24
+ def self.files_in_path(path)
25
+ return Dir[File.join("#{path}","/**/*")].select {|f| File.file?(f)}
26
+ end
27
+
28
+ # Returns the destination viper path
29
+ # @return Destination root path
30
+ def self.destination_viper_path(path, name)
31
+ expand_path = File.expand_path(path)
32
+ return File.join(expand_path,name)
33
+ end
34
+
35
+ # Copy a system item to another place
36
+ def self.copy(from, to)
37
+ to_expand_path = File.expand_path(to)
38
+ from_expand_path = File.expand_path(from)
39
+ FileUtils.mkdir_p (to_expand_path)
40
+ FileUtils.copy_entry(from_expand_path, to_expand_path)
41
+ end
42
+
43
+ # Move a system item to another place
44
+ def self.move(from, to)
45
+ to_expand_path = File.expand_path(to)
46
+ from_expand_path = File.expand_path(from)
47
+ FileUtils.move(from_expand_path, to_expand_path)
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ module Vipergen
2
+ # Cosntants
3
+ class Generator
4
+ # Constants
5
+ LANGUAGES = ["swift", "objc"]
6
+ REPLACEMENT_KEY = "VIPER"
7
+ AUTHOR_REPLACEMENT_KEY = "AUTHOR"
8
+ YEAR_REPLACEMENT_KEY = "YEAR"
9
+ COMPANY_REPLACEMENT_KEY = "COMPANY"
10
+
11
+ # Main method that generate the VIPER files structure
12
+ def self.generate_viper(template, language, name, path, author, company)
13
+ puts "Generating VIPER-Module"
14
+ puts "Template: #{template}"
15
+ puts "Language: #{language}"
16
+ puts "Name: #{name}"
17
+ puts "Path: #{path}"
18
+ puts "Author: #{author}"
19
+ puts "Company: #{company}"
20
+ path_from = Vipergen::FileManager.path_from(template, language)
21
+ path_to = Vipergen::FileManager.destination_viper_path(path, name)
22
+ Vipergen::FileManager.copy(path_from, path_to)
23
+ files = Vipergen::FileManager.files_in_path(path_to)
24
+ rename_files(files, name, author, company)
25
+ end
26
+
27
+ # Rename all the files in the files array
28
+ # - It renames the name of the file
29
+ # - It renames the content of the file
30
+ def self.rename_files(files, name, author, company)
31
+ files.each do |file|
32
+ raise SyntaxError unless file.include? (Vipergen::Generator::REPLACEMENT_KEY)
33
+ rename_file(file, name, author, company)
34
+ end
35
+ end
36
+
37
+ # Rename a given file
38
+ # - It renames the name of the file
39
+ # - It renames the content of the file
40
+ def self.rename_file(file, name, author, company)
41
+ new_path = file.gsub((Vipergen::Generator::REPLACEMENT_KEY), name)
42
+ Vipergen::FileManager.move(file, new_path)
43
+ rename_file_content(new_path, name, author, company)
44
+ end
45
+
46
+ # Rename the file content
47
+ # @return: An String with the every VIPER replaced by 'name'
48
+ def self.rename_file_content(filename, name, author, company)
49
+ # Reading content
50
+ file = File.open(filename, "rb")
51
+ content = file.read
52
+ file.close
53
+
54
+ # Replacing content
55
+ content = content.gsub((Vipergen::Generator::REPLACEMENT_KEY), name)
56
+ content = content.gsub((Vipergen::Generator::AUTHOR_REPLACEMENT_KEY), author)
57
+ content = content.gsub((Vipergen::Generator::YEAR_REPLACEMENT_KEY), "#{Time.new.year}")
58
+ content = content.gsub((Vipergen::Generator::COMPANY_REPLACEMENT_KEY), company)
59
+
60
+ # Saving content with replaced string
61
+ File.open(filename, "w+") do |file|
62
+ file.write(content)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ require 'yaml'
2
+ module Vipergen
3
+ class TemplateManager
4
+
5
+ # Returns the templates dir
6
+ def self.templates_dir
7
+ t = "#{File.expand_path File.dirname(__FILE__)}/../templates"
8
+ end
9
+
10
+ # Get the available templates paths
11
+ # @return Array[String] with available templates paths
12
+ def self.templates_paths()
13
+ template_dir = Vipergen::TemplateManager.templates_dir
14
+ return Vipergen::DirUtils.directories_in(template_dir)
15
+ end
16
+
17
+ # Get the templates names
18
+ # @return Array[String] with templates names (got from the folder)
19
+ def self.templates()
20
+ templates_paths.map{|template_path| template_name_from_path(template_path)}
21
+ end
22
+
23
+ # Returns the template name from a given template_path
24
+ # @return String with the template name
25
+ def self.template_name_from_path(template_path)
26
+ return template_path.split("/").last
27
+ end
28
+
29
+ # Returns the description of all the templates available
30
+ # @return String with the entire description
31
+ def self.templates_description()
32
+ description = "\nAvailable templates \n"
33
+ description += "------------------- \n"
34
+ self.templates_paths.each do |template_path|
35
+ description += "> #{template_description(template_path)} \n"
36
+ end
37
+ return description
38
+ end
39
+
40
+ # Returns the description of a given template
41
+ # @param template String with the template path whose description is going to be returned
42
+ # @return String with the template description
43
+ def self.template_description(template_path)
44
+ template_description = ""
45
+
46
+ # Reading yaml
47
+ template_content = YAML.load_file(File.join(template_path,'viperspec.yml'))
48
+
49
+ # Generating string
50
+ template_description+= "| #{template_name_from_path(template_path)} by #{template_content["author"]} |: #{template_content["template_description"]}"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,4 @@
1
+ module Vipergen
2
+ NAME = "vipergeng"
3
+ VERSION = "0.2.7"
4
+ end
@@ -0,0 +1,21 @@
1
+ require 'thor'
2
+ require 'vipergen'
3
+
4
+ module Vipergen
5
+ class ViperThor < Thor
6
+ desc "generate MODULE", "Generate a VIPER module"
7
+ option :language, :required => false, :default => 'swift', :type => :string, :desc => "The language of the generated module (swift, objc)"
8
+ option :template, :required => false, :default => 'default', :type => :string , :desc => "Template for the generation"
9
+ option :path, :required => true, :type => :string , :desc => "Path where the output module is going to be saved"
10
+ option :author, :required => false, :default => 'VIPER', :type => :string , :desc => "Author to be specified on the file's header. Otherwise VIPER will be used"
11
+ option :company, :required => false, :default => 'Company', :type => :string, :desc => "Company to be specified on the file's header. Otherwise Company will be used"
12
+ def generate(name)
13
+ Vipergen::Generator.generate_viper(options[:template], options[:language], name, options[:path], options[:author], options[:company])
14
+ end
15
+
16
+ desc "templates", "Get a list of available templates"
17
+ def templates()
18
+ puts Vipergen::TemplateManager.templates_description()
19
+ end
20
+ end
21
+ end
data/lib/vipergen.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'vipergen/generator'
2
+ require 'vipergen/filemanager'
3
+ require 'vipergen/dirutils'
4
+ require 'vipergen/version'
5
+ require 'vipergen/viperthor'
6
+ require 'vipergen/templatemanager'
@@ -0,0 +1,4 @@
1
+ require 'vipergen'
2
+ require "codeclimate-test-reporter"
3
+
4
+ CodeClimate::TestReporter.start
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vipergen do
4
+ context "when generating path" do
5
+ it "should return nil if no valid template" do
6
+ valid_template = Vipergen::FileManager.is_template_valid("asdgas")
7
+ expect(valid_template).to be(false)
8
+ end
9
+ it "should return nil if no valid language" do
10
+ valid_template = Vipergen::FileManager.is_template_valid("asdgas")
11
+ expect(valid_template).to be(false)
12
+ end
13
+ it "should return nil if no valid language when getting path" do
14
+ path = Vipergen::FileManager.path_from("default", "asgass")
15
+ expect(path).to be(nil)
16
+ end
17
+ it "should return nil if no valid template when getting path" do
18
+ path = Vipergen::FileManager.path_from("asga", "swift")
19
+ expect(path).to be(nil)
20
+ end
21
+ it "should append the name to the given user path" do
22
+ to_path = Vipergen::FileManager.destination_viper_path("path/", "pepito")
23
+ expect(to_path).to eq(File.join(File.expand_path("path/"),"pepito"))
24
+ end
25
+ end
26
+ context "copying a folder to a diferent place" do
27
+ before (:each) do
28
+ Dir.mkdir 'foo'
29
+ Dir.mkdir 'foo/subfoo'
30
+ end
31
+
32
+ it "should copy a given folder properly" do
33
+ Vipergen::FileManager.copy('foo','test_foo')
34
+ expect(File.directory?('test_foo/subfoo')).to eq(true)
35
+ end
36
+
37
+ after (:each) do
38
+ FileUtils.rm_rf('foo')
39
+ FileUtils.rm_rf('test_foo')
40
+ end
41
+ end
42
+ end
43
+
44
+ describe Vipergen::Generator do
45
+ context "when renaming file content" do
46
+ before (:each) do
47
+ File.open("test.txt", 'w') {|f| f.write("I'm a #{Vipergen::Generator::REPLACEMENT_KEY} file by #{Vipergen::Generator::AUTHOR_REPLACEMENT_KEY} @ #{Vipergen::Generator::COMPANY_REPLACEMENT_KEY} on #{Vipergen::Generator::YEAR_REPLACEMENT_KEY}") }
48
+ end
49
+
50
+ it "should rename every VIPER word to the given name" do
51
+ Vipergen::Generator.rename_file_content("test.txt","RENAMED", "pepito", "ViperGen")
52
+ file = File.open("test.txt", "rb")
53
+ content = file.read
54
+ expect(content).to eq("I'm a RENAMED file by pepito @ ViperGen on #{Time.new.year}")
55
+ end
56
+
57
+ after (:each) do
58
+ FileUtils.rm('test.txt')
59
+ end
60
+ end
61
+
62
+ context "when renaming file" do
63
+ before (:each) do
64
+ File.open("#{Vipergen::Generator::REPLACEMENT_KEY}test.txt", 'w') {|f| f.write("I'm a #{Vipergen::Generator::REPLACEMENT_KEY} file") }
65
+ end
66
+
67
+ it "every file should be renamed in rename_files" do
68
+ expect(Vipergen::Generator).to receive(:rename_file)
69
+ Vipergen::Generator.rename_files(["#{Vipergen::Generator::REPLACEMENT_KEY}file.txt"], "MyModule", "Pepi", "ViperGen")
70
+ end
71
+
72
+ it "should raise a SyntaxError exeption if there's a file in the template without the proper name" do
73
+ expect{Vipergen::Generator.rename_files(["asgasgs.txt"], "MyModule", "Pepi", "ViperGen")}.to raise_error
74
+ end
75
+
76
+ it "should rename the VIPER in name to the given name" do
77
+ file = "#{Vipergen::Generator::REPLACEMENT_KEY}test.txt"
78
+ name = "RENAMED"
79
+ author = "PEPI"
80
+ company = "ViperGen"
81
+ Vipergen::Generator.rename_file(file, name, author, company)
82
+ expect(File.exist? "RENAMEDtest.txt").to eq(true)
83
+ end
84
+
85
+ it "should rename the file content after the file name rename" do
86
+ file = "#{Vipergen::Generator::REPLACEMENT_KEY}test.txt"
87
+ name = "RENAMED"
88
+ author = "PEPI"
89
+ company = "ViperGen"
90
+ expect(Vipergen::Generator).to receive(:rename_file_content)
91
+ Vipergen::Generator.rename_file(file, name, author, company)
92
+ end
93
+
94
+ after (:each) do
95
+ File.delete "#{Vipergen::Generator::REPLACEMENT_KEY}test.txt" if File.exist? "#{Vipergen::Generator::REPLACEMENT_KEY}test.txt"
96
+ File.delete "RENAMEDtest.txt" if File.exist? "RENAMEDtest.txt"
97
+ end
98
+ end
99
+ end
100
+
101
+ describe Vipergen::DirUtils do
102
+ context "getting directories" do
103
+ before (:each) do
104
+ Dir.mkdir 'foo'
105
+ Dir.mkdir 'foo/subfoo'
106
+ end
107
+
108
+ it "should return the directories inside a given one" do
109
+ expect(Vipergen::DirUtils.directories_in('foo').count).to eq(1)
110
+ end
111
+
112
+ after (:each) do
113
+ FileUtils.rm_rf('foo')
114
+ end
115
+ end
116
+ end
117
+
118
+ describe Vipergen::TemplateManager do
119
+ context "getting templates" do
120
+ before (:each) do
121
+ Dir.mkdir 'foo'
122
+ Dir.mkdir 'foo/subfoo'
123
+ Dir.mkdir 'foo/subfoo2'
124
+ end
125
+
126
+ it "should return the proper templates in templates directory" do
127
+ Vipergen::TemplateManager.stub(:templates_dir).and_return('foo/')
128
+ expect(Vipergen::TemplateManager.templates_paths.count).to eq(2)
129
+ end
130
+
131
+ after (:each) do
132
+ FileUtils.rm_rf('foo')
133
+ end
134
+ end
135
+ end