amee 2.6.0 → 2.7.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 (46) hide show
  1. data/Gemfile +18 -0
  2. data/README +2 -2
  3. data/Rakefile +31 -0
  4. data/VERSION +1 -0
  5. data/amee-ruby.gemspec +147 -0
  6. data/examples/view_profile_item.rb +32 -0
  7. data/lib/amee.rb +0 -1
  8. data/lib/amee/profile_item.rb +27 -6
  9. data/spec/amee_spec.rb +25 -0
  10. data/spec/cache_spec.rb +152 -0
  11. data/spec/connection_spec.rb +295 -0
  12. data/spec/data_category_spec.rb +259 -0
  13. data/spec/data_item_spec.rb +224 -0
  14. data/spec/data_item_value_history_spec.rb +311 -0
  15. data/spec/data_item_value_spec.rb +231 -0
  16. data/spec/data_object_spec.rb +9 -0
  17. data/spec/drill_down_spec.rb +163 -0
  18. data/spec/fixtures/AD63A83B4D41.json +1 -0
  19. data/spec/fixtures/AD63A83B4D41.xml +1 -0
  20. data/spec/fixtures/create_item.json +1 -0
  21. data/spec/fixtures/create_item.xml +1 -0
  22. data/spec/fixtures/data.json +1 -0
  23. data/spec/fixtures/data.xml +1 -0
  24. data/spec/fixtures/data_home_energy_quantity.xml +146 -0
  25. data/spec/fixtures/data_home_energy_quantity_biodiesel.xml +177 -0
  26. data/spec/fixtures/data_transport_car_generic_drill_fuel_diesel.xml +33 -0
  27. data/spec/fixtures/empty.json +1 -0
  28. data/spec/fixtures/empty.xml +1 -0
  29. data/spec/fixtures/parse_test.xml +22 -0
  30. data/spec/fixtures/v0_data_transport_transport_drill_transportType_Car1.xml +20 -0
  31. data/spec/item_definition_spec.rb +313 -0
  32. data/spec/item_value_definition_spec.rb +253 -0
  33. data/spec/logger_spec.rb +16 -0
  34. data/spec/object_spec.rb +44 -0
  35. data/spec/parse_helper_spec.rb +72 -0
  36. data/spec/profile_category_spec.rb +565 -0
  37. data/spec/profile_item_spec.rb +451 -0
  38. data/spec/profile_item_value_spec.rb +196 -0
  39. data/spec/profile_object_spec.rb +24 -0
  40. data/spec/profile_spec.rb +88 -0
  41. data/spec/rails_spec.rb +48 -0
  42. data/spec/spec.opts +2 -0
  43. data/spec/spec_helper.rb +56 -0
  44. data/spec/user_spec.rb +249 -0
  45. metadata +189 -55
  46. data/lib/amee/version.rb +0 -10
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "activesupport", "~> 2.3.5"
4
+ gem "json"
5
+ gem "log4r"
6
+ gem "nokogiri", "~> 1.4.3.1"
7
+
8
+ # Add dependencies to develop your gem here.
9
+ # Include everything needed to run rake, tests, features, etc.
10
+ group :development do
11
+ gem "bundler", "~> 1.0.0"
12
+ gem "jeweler", "~> 1.6.4"
13
+ gem 'rspec', '1.3.0'
14
+ gem 'rcov'
15
+ gem 'rspec_spinner', '1.1.3'
16
+ gem 'activerecord', "~> 2.3.5" # To test Rails integration
17
+ gem 'flexmock'
18
+ end
data/README CHANGED
@@ -6,9 +6,9 @@ Licensed under the MIT license (See COPYING file for details)
6
6
 
7
7
  Author: James Smith (james@floppy.org.uk / http://www.floppy.org.uk)
8
8
 
9
- Homepage: http://github.com/Floppy/amee-ruby
9
+ Homepage: http://github.com/AMEE/amee-ruby
10
10
 
11
- Documentation: http://docs.github.com/Floppy/amee-ruby
11
+ Documentation: http://docs.github.com/AMEE/amee-ruby
12
12
 
13
13
  == INSTALLATION
14
14
 
@@ -0,0 +1,31 @@
1
+ require 'rake'
2
+ require 'spec'
3
+ require 'spec/rake/spectask'
4
+ require 'rake/rdoctask'
5
+ require 'rubygems/specification'
6
+ require 'rake/gempackagetask'
7
+
8
+ task :default => [:spec]
9
+
10
+ Spec::Rake::SpecTask.new do |t|
11
+ t.spec_opts = ['--options', "spec/spec.opts"]
12
+ t.spec_files = FileList['spec/**/*_spec.rb']
13
+ t.rcov = true
14
+ t.rcov_opts = ['--exclude', 'spec,/usr,/Library']
15
+ end
16
+
17
+ Rake::RDocTask.new { |rdoc|
18
+ rdoc.rdoc_dir = 'doc'
19
+ rdoc.title = "AMEE-Ruby - RDoc documentation"
20
+ rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
21
+ rdoc.options << '--charset' << 'utf-8'
22
+ rdoc.rdoc_files.include('README', 'COPYING')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ }
25
+
26
+ # Gem build task - load gemspec from file
27
+ gemspec = File.read('amee-ruby.gemspec')
28
+ spec = eval("#{gemspec}")
29
+ Rake::GemPackageTask.new(spec) do |p|
30
+ p.gem_spec = spec
31
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.7.0
@@ -0,0 +1,147 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
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{amee-ruby}
8
+ s.version = "2.7.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["James Smith"]
12
+ s.date = %q{2011-08-09}
13
+ s.default_executable = %q{ameesh}
14
+ s.email = %q{james@floppy.org.uk}
15
+ s.executables = ["ameesh"]
16
+ s.extra_rdoc_files = [
17
+ "README"
18
+ ]
19
+ s.files = [
20
+ "CHANGELOG",
21
+ "COPYING",
22
+ "Gemfile",
23
+ "README",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "amee-ruby.gemspec",
27
+ "amee.example.yml",
28
+ "bin/ameesh",
29
+ "cacert.pem",
30
+ "examples/create_profile.rb",
31
+ "examples/create_profile_item.rb",
32
+ "examples/list_profiles.rb",
33
+ "examples/view_data_category.rb",
34
+ "examples/view_data_item.rb",
35
+ "examples/view_profile_item.rb",
36
+ "init.rb",
37
+ "lib/amee.rb",
38
+ "lib/amee/collection.rb",
39
+ "lib/amee/connection.rb",
40
+ "lib/amee/data_category.rb",
41
+ "lib/amee/data_item.rb",
42
+ "lib/amee/data_item_value.rb",
43
+ "lib/amee/data_item_value_history.rb",
44
+ "lib/amee/data_object.rb",
45
+ "lib/amee/drill_down.rb",
46
+ "lib/amee/exceptions.rb",
47
+ "lib/amee/item_definition.rb",
48
+ "lib/amee/item_value_definition.rb",
49
+ "lib/amee/logger.rb",
50
+ "lib/amee/object.rb",
51
+ "lib/amee/pager.rb",
52
+ "lib/amee/parse_helper.rb",
53
+ "lib/amee/profile.rb",
54
+ "lib/amee/profile_category.rb",
55
+ "lib/amee/profile_item.rb",
56
+ "lib/amee/profile_item_value.rb",
57
+ "lib/amee/profile_object.rb",
58
+ "lib/amee/rails.rb",
59
+ "lib/amee/shell.rb",
60
+ "lib/amee/user.rb",
61
+ "rails/init.rb",
62
+ "spec/amee_spec.rb",
63
+ "spec/cache_spec.rb",
64
+ "spec/connection_spec.rb",
65
+ "spec/data_category_spec.rb",
66
+ "spec/data_item_spec.rb",
67
+ "spec/data_item_value_history_spec.rb",
68
+ "spec/data_item_value_spec.rb",
69
+ "spec/data_object_spec.rb",
70
+ "spec/drill_down_spec.rb",
71
+ "spec/fixtures/AD63A83B4D41.json",
72
+ "spec/fixtures/AD63A83B4D41.xml",
73
+ "spec/fixtures/create_item.json",
74
+ "spec/fixtures/create_item.xml",
75
+ "spec/fixtures/data.json",
76
+ "spec/fixtures/data.xml",
77
+ "spec/fixtures/data_home_energy_quantity.xml",
78
+ "spec/fixtures/data_home_energy_quantity_biodiesel.xml",
79
+ "spec/fixtures/data_transport_car_generic_drill_fuel_diesel.xml",
80
+ "spec/fixtures/empty.json",
81
+ "spec/fixtures/empty.xml",
82
+ "spec/fixtures/parse_test.xml",
83
+ "spec/fixtures/v0_data_transport_transport_drill_transportType_Car1.xml",
84
+ "spec/item_definition_spec.rb",
85
+ "spec/item_value_definition_spec.rb",
86
+ "spec/logger_spec.rb",
87
+ "spec/object_spec.rb",
88
+ "spec/parse_helper_spec.rb",
89
+ "spec/profile_category_spec.rb",
90
+ "spec/profile_item_spec.rb",
91
+ "spec/profile_item_value_spec.rb",
92
+ "spec/profile_object_spec.rb",
93
+ "spec/profile_spec.rb",
94
+ "spec/rails_spec.rb",
95
+ "spec/spec.opts",
96
+ "spec/spec_helper.rb",
97
+ "spec/user_spec.rb"
98
+ ]
99
+ s.homepage = %q{http://github.com/Floppy/amee-ruby}
100
+ s.licenses = ["MIT"]
101
+ s.require_paths = ["lib"]
102
+ s.rubygems_version = %q{1.6.2}
103
+ s.summary = %q{Ruby interface to the AMEE carbon calculator}
104
+
105
+ if s.respond_to? :specification_version then
106
+ s.specification_version = 3
107
+
108
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
109
+ s.add_runtime_dependency(%q<activesupport>, ["~> 2.3.5"])
110
+ s.add_runtime_dependency(%q<json>, [">= 0"])
111
+ s.add_runtime_dependency(%q<log4r>, [">= 0"])
112
+ s.add_runtime_dependency(%q<nokogiri>, ["~> 1.4.3.1"])
113
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
114
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
115
+ s.add_development_dependency(%q<rspec>, ["= 1.3.0"])
116
+ s.add_development_dependency(%q<rcov>, [">= 0"])
117
+ s.add_development_dependency(%q<rspec_spinner>, ["= 1.1.3"])
118
+ s.add_development_dependency(%q<activerecord>, ["~> 2.3.5"])
119
+ s.add_development_dependency(%q<flexmock>, [">= 0"])
120
+ else
121
+ s.add_dependency(%q<activesupport>, ["~> 2.3.5"])
122
+ s.add_dependency(%q<json>, [">= 0"])
123
+ s.add_dependency(%q<log4r>, [">= 0"])
124
+ s.add_dependency(%q<nokogiri>, ["~> 1.4.3.1"])
125
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
126
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
127
+ s.add_dependency(%q<rspec>, ["= 1.3.0"])
128
+ s.add_dependency(%q<rcov>, [">= 0"])
129
+ s.add_dependency(%q<rspec_spinner>, ["= 1.1.3"])
130
+ s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
131
+ s.add_dependency(%q<flexmock>, [">= 0"])
132
+ end
133
+ else
134
+ s.add_dependency(%q<activesupport>, ["~> 2.3.5"])
135
+ s.add_dependency(%q<json>, [">= 0"])
136
+ s.add_dependency(%q<log4r>, [">= 0"])
137
+ s.add_dependency(%q<nokogiri>, ["~> 1.4.3.1"])
138
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
139
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
140
+ s.add_dependency(%q<rspec>, ["= 1.3.0"])
141
+ s.add_dependency(%q<rcov>, [">= 0"])
142
+ s.add_dependency(%q<rspec_spinner>, ["= 1.1.3"])
143
+ s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
144
+ s.add_dependency(%q<flexmock>, [">= 0"])
145
+ end
146
+ end
147
+
@@ -0,0 +1,32 @@
1
+ dir = File.dirname(__FILE__) + '/../lib'
2
+ $LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
3
+
4
+ #require 'rubygems'
5
+ require 'amee'
6
+ require 'optparse'
7
+
8
+ # Command-line options - get username, password, and server
9
+ options = {}
10
+ OptionParser.new do |opts|
11
+ opts.on("-u", "--username USERNAME", "AMEE username") do |u|
12
+ options[:username] = u
13
+ end
14
+ opts.on("-p", "--password PASSWORD", "AMEE password") do |p|
15
+ options[:password] = p
16
+ end
17
+ opts.on("-s", "--server SERVER", "AMEE server") do |s|
18
+ options[:server] = s
19
+ end
20
+ end.parse!
21
+
22
+ # Connect
23
+ connection = AMEE::Connection.new(options[:server], options[:username], options[:password])
24
+
25
+ # Create a new profile item
26
+ item = AMEE::Profile::Item.get(connection, ARGV[0])
27
+ puts "loaded item #{item.name}"
28
+ item.values.each do |x|
29
+ puts " - #{x[:name]}: #{x[:value]}"
30
+ end
31
+ puts " - total: #{item.total_amount}"
32
+
@@ -29,7 +29,6 @@ class String
29
29
  end
30
30
 
31
31
  require 'amee/logger'
32
- require 'amee/version'
33
32
  require 'amee/exceptions'
34
33
  require 'amee/connection'
35
34
  require 'amee/parse_helper'
@@ -305,20 +305,41 @@ module AMEE
305
305
  options[:duration] = "PT#{options[:duration] * 86400}S"
306
306
  end
307
307
  # Send data to path
308
+ options.merge!(:representation => 'full') if (connection.version >= 2) && (get_item == true)
308
309
  options.merge! :dataItemUid => data_item_uid
310
+ # POST
309
311
  response = connection.post(path, options)
312
+ # Parse response
313
+ category = response.body.empty? ? nil : Category.parse(connection, response.body, options)
310
314
  if response['Location']
311
315
  location = response['Location'].match("https??://.*?(/.*)")[1]
312
316
  else
313
- category = Category.parse(connection, response.body, nil)
314
317
  location = category.full_path + "/" + category.items[0][:path]
315
318
  end
316
319
  if get_item == true
317
- get_options = {}
318
- get_options[:returnUnit] = options[:returnUnit] if options[:returnUnit]
319
- get_options[:returnPerUnit] = options[:returnPerUnit] if options[:returnPerUnit]
320
- get_options[:format] = format if format
321
- return AMEE::Profile::Item.get(connection, location, get_options)
320
+ if connection.version >= 2
321
+ item = category.items.first
322
+ item[:connection] = category.connection
323
+ item[:profile_uid] = category.profile_uid
324
+ item[:data_item_label] = item.delete(:dataItemLabel)
325
+ item[:data_item_uid] = item.delete(:dataItemUid)
326
+ item[:start_date] = item.delete(:startDate)
327
+ item[:end_date] = item.delete(:endDate)
328
+ item[:total_amount] = item.delete(:amount) || item.delete(:amountPerMonth)
329
+ item[:total_amount_unit] = item.delete(:amount_unit) || "kg/month"
330
+ values = []
331
+ item[:values].each do |k,v|
332
+ values << v.merge(:path => k.to_s)
333
+ end
334
+ item[:values] = values
335
+ return AMEE::Profile::Item.new(item)
336
+ else
337
+ get_options = {}
338
+ get_options[:returnUnit] = options[:returnUnit] if options[:returnUnit]
339
+ get_options[:returnPerUnit] = options[:returnPerUnit] if options[:returnPerUnit]
340
+ get_options[:format] = format if format
341
+ return AMEE::Profile::Item.get(connection, location, get_options)
342
+ end
322
343
  else
323
344
  return location
324
345
  end
@@ -0,0 +1,25 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe "AMEE module" do
4
+
5
+ it "should cope if json gem isn't available" do
6
+ # Monkeypatch Kernel#require to make sure that require 'json'
7
+ # raises a LoadError
8
+ module Kernel
9
+ def require_with_mock(string)
10
+ raise LoadError.new if string == 'json'
11
+ require_without_mock(string)
12
+ end
13
+ alias_method :require_without_mock, :require
14
+ alias_method :require, :require_with_mock
15
+ end
16
+ # Remove amee.rb from required file list so we can load it again
17
+ $".delete_if{|x| x.include? 'amee.rb'}
18
+ # Require file - require 'json' should throw a LoadError,
19
+ # but we should cope with it OK.
20
+ lambda {
21
+ require 'amee'
22
+ }.should_not raise_error
23
+ end
24
+
25
+ end
@@ -0,0 +1,152 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+ require 'ostruct'
3
+
4
+ describe AMEE::Connection do
5
+
6
+ describe 'without caching' do
7
+
8
+ it "doesn't cache GET requests" do
9
+ flexmock(Net::HTTP).new_instances do |mock|
10
+ mock.should_receive(:start => nil)
11
+ mock.should_receive(:request).twice.and_return(flexmock(:code => '200', :body => fixture('data_home_energy_quantity.xml')))
12
+ mock.should_receive(:finish => nil)
13
+ end
14
+ @connection = AMEE::Connection.new("server.example.com", "username", "password")
15
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
16
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
17
+ end
18
+
19
+ end
20
+
21
+ describe 'with caching' do
22
+
23
+ def setup_connection
24
+ @connection = AMEE::Connection.new("server.example.com", "username", "password", :cache => :memory_store)
25
+ end
26
+
27
+ it "caches GET requests" do
28
+ flexmock(Net::HTTP).new_instances do |mock|
29
+ mock.should_receive(:start => nil)
30
+ mock.should_receive(:request).once.and_return(OpenStruct.new(:code => '200', :body => fixture('data_home_energy_quantity.xml')))
31
+ mock.should_receive(:finish => nil)
32
+ end
33
+ setup_connection
34
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
35
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
36
+ end
37
+
38
+ it "allows complete cache clear" do
39
+ flexmock(Net::HTTP).new_instances do |mock|
40
+ mock.should_receive(:start => nil)
41
+ mock.should_receive(:request).twice.and_return(OpenStruct.new(:code => '200', :body => fixture('data_home_energy_quantity.xml')))
42
+ mock.should_receive(:finish => nil)
43
+ end
44
+ setup_connection
45
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
46
+ @connection.expire_all
47
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
48
+ end
49
+
50
+ it "allows manual cache expiry for objects" do
51
+ flexmock(Net::HTTP).new_instances do |mock|
52
+ mock.should_receive(:start => nil)
53
+ mock.should_receive(:request).twice.and_return(OpenStruct.new(:code => '200', :body => fixture('data_home_energy_quantity.xml')))
54
+ mock.should_receive(:finish => nil)
55
+ end
56
+ setup_connection
57
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
58
+ c.expire_cache
59
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
60
+ end
61
+
62
+ it "object expiry invalidates objectes further down the tree" do
63
+ flexmock(Net::HTTP).new_instances do |mock|
64
+ mock.should_receive(:start => nil)
65
+ mock.should_receive(:request).once.and_return(OpenStruct.new(:code => '200', :body => fixture('data_home_energy_quantity.xml')))
66
+ mock.should_receive(:request).twice.and_return(OpenStruct.new(:code => '200', :body => fixture('data_home_energy_quantity_biodiesel.xml')))
67
+ mock.should_receive(:finish => nil)
68
+ end
69
+ setup_connection
70
+ c = AMEE::Data::Category.get(@connection, '/data/home/energy/quantity')
71
+ i = c.item :label => 'biodiesel'
72
+ c.expire_cache
73
+ i = c.item :label => 'biodiesel'
74
+ end
75
+
76
+ describe 'and automatic invalidation' do
77
+
78
+ def test_invalidation_sequence(interactions)
79
+ flexmock(Net::HTTP).new_instances do |mock|
80
+ mock.should_receive(:start => nil)
81
+ interactions.each do |path, action, result|
82
+ mock.should_receive(:request).once.and_return(OpenStruct.new(:code => '200', :body => path)) if result
83
+ end
84
+ mock.should_receive(:finish => nil)
85
+ end
86
+ setup_connection
87
+ interactions.each do |path, action, result|
88
+ if action
89
+ @connection.send(action, path).body.should == path
90
+ end
91
+ end
92
+ end
93
+
94
+ it "handles PUT requests" do
95
+ test_invalidation_sequence([
96
+ ["/parent/object", :get, true],
97
+ ["/parent", :get, true],
98
+ ["/parent/object/child", :get, true],
99
+ ["/parent/sibling", :get, true],
100
+ ["/uncle/cousin", :get, true],
101
+ ["/uncle", :get, true],
102
+ ["/parent/object", :put, true],
103
+ ["/parent/object", :get, true],
104
+ ["/parent", :get, true],
105
+ ["/parent/object/child", :get, true],
106
+ ["/parent/sibling", :get, true],
107
+ ["/uncle/cousin", :get, false],
108
+ ["/uncle", :get, false],
109
+ ])
110
+ end
111
+
112
+ it "handles POST requests" do
113
+ test_invalidation_sequence([
114
+ ["/parent/object", :get, true],
115
+ ["/parent", :get, true],
116
+ ["/parent/object/child", :get, true],
117
+ ["/parent/sibling", :get, true],
118
+ ["/uncle/cousin", :get, true],
119
+ ["/uncle", :get, true],
120
+ ["/parent/object", :post, true],
121
+ ["/parent/object", :get, true],
122
+ ["/parent", :get, false],
123
+ ["/parent/object/child", :get, true],
124
+ ["/parent/sibling", :get, false],
125
+ ["/uncle/cousin", :get, false],
126
+ ["/uncle", :get, false],
127
+ ])
128
+ end
129
+
130
+ it "handles DELETE requests" do
131
+ test_invalidation_sequence([
132
+ ["/parent/object", :get, true],
133
+ ["/parent", :get, true],
134
+ ["/parent/object/child", :get, true],
135
+ ["/parent/sibling", :get, true],
136
+ ["/uncle/cousin", :get, true],
137
+ ["/uncle", :get, true],
138
+ ["/parent/object", :delete, true],
139
+ ["/parent/object", :get, true],
140
+ ["/parent", :get, true],
141
+ ["/parent/object/child", :get, true],
142
+ ["/parent/sibling", :get, true],
143
+ ["/uncle/cousin", :get, false],
144
+ ["/uncle", :get, false],
145
+ ])
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+
152
+ end