codefumes 0.1.10 → 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.
Files changed (68) hide show
  1. data/Gemfile +19 -0
  2. data/Gemfile.lock +135 -0
  3. data/History.txt +12 -0
  4. data/LICENSE +20 -0
  5. data/Manifest.txt +40 -19
  6. data/README.txt +11 -29
  7. data/Rakefile +15 -10
  8. data/bin/fumes +214 -0
  9. data/config/website.yml +2 -0
  10. data/cucumber.yml +2 -0
  11. data/features/claiming_a_project.feature +46 -0
  12. data/features/deleting_a_project.feature +32 -0
  13. data/features/releasing_a_project.feature +50 -0
  14. data/features/step_definitions/cli_steps.rb +98 -0
  15. data/features/step_definitions/common_steps.rb +168 -0
  16. data/features/step_definitions/filesystem_steps.rb +19 -0
  17. data/features/storing_user_api_key.feature +41 -0
  18. data/features/support/common.rb +29 -0
  19. data/features/support/env.rb +24 -0
  20. data/features/support/matchers.rb +11 -0
  21. data/features/synchronizing_repository_with_project.feature +33 -0
  22. data/lib/codefumes.rb +10 -8
  23. data/lib/codefumes/api.rb +20 -11
  24. data/lib/codefumes/api/build.rb +139 -0
  25. data/lib/codefumes/api/claim.rb +74 -0
  26. data/lib/codefumes/api/commit.rb +150 -0
  27. data/lib/codefumes/api/payload.rb +93 -0
  28. data/lib/codefumes/api/project.rb +158 -0
  29. data/lib/codefumes/cli_helpers.rb +54 -0
  30. data/lib/codefumes/config_file.rb +3 -2
  31. data/lib/codefumes/errors.rb +21 -0
  32. data/lib/codefumes/exit_codes.rb +10 -0
  33. data/lib/codefumes/harvester.rb +113 -0
  34. data/lib/codefumes/quick_build.rb +43 -0
  35. data/lib/codefumes/quick_metric.rb +20 -0
  36. data/lib/codefumes/source_control.rb +137 -0
  37. data/lib/integrity_notifier/codefumes.haml +11 -0
  38. data/lib/integrity_notifier/codefumes.rb +62 -0
  39. data/spec/codefumes/{build_spec.rb → api/build_spec.rb} +14 -24
  40. data/spec/codefumes/{claim_spec.rb → api/claim_spec.rb} +42 -3
  41. data/spec/codefumes/{commit_spec.rb → api/commit_spec.rb} +34 -24
  42. data/spec/codefumes/api/payload_spec.rb +148 -0
  43. data/spec/codefumes/api/project_spec.rb +286 -0
  44. data/spec/codefumes/api_spec.rb +38 -15
  45. data/spec/codefumes/config_file_spec.rb +69 -13
  46. data/spec/codefumes/harvester_spec.rb +118 -0
  47. data/spec/codefumes/source_control_spec.rb +199 -0
  48. data/spec/codefumes_service_helpers.rb +23 -19
  49. data/spec/fixtures/sample_project_dirs/no_scm/description +4 -0
  50. data/spec/spec_helper.rb +1 -0
  51. data/tasks/cucumber.rake +11 -0
  52. metadata +145 -60
  53. data/bin/cf_claim_project +0 -9
  54. data/bin/cf_release_project +0 -10
  55. data/bin/cf_store_credentials +0 -10
  56. data/lib/cf_claim_project/cli.rb +0 -95
  57. data/lib/cf_release_project/cli.rb +0 -76
  58. data/lib/cf_store_credentials/cli.rb +0 -50
  59. data/lib/codefumes/build.rb +0 -131
  60. data/lib/codefumes/claim.rb +0 -57
  61. data/lib/codefumes/commit.rb +0 -144
  62. data/lib/codefumes/payload.rb +0 -103
  63. data/lib/codefumes/project.rb +0 -129
  64. data/spec/cf_claim_project/cli_spec.rb +0 -17
  65. data/spec/cf_release_project/cli_spec.rb +0 -41
  66. data/spec/cf_store_credentials/cli_spec.rb +0 -28
  67. data/spec/codefumes/payload_spec.rb +0 -155
  68. data/spec/codefumes/project_spec.rb +0 -274
@@ -0,0 +1,29 @@
1
+ module CommonHelpers
2
+ def in_tmp_folder(&block)
3
+ FileUtils.chdir(@tmp_root, &block)
4
+ end
5
+
6
+ def in_project_folder(&block)
7
+ project_folder = @active_project_folder || @tmp_root
8
+ FileUtils.chdir(project_folder, &block)
9
+ end
10
+
11
+ def in_home_folder(&block)
12
+ FileUtils.chdir(@home_path, &block)
13
+ end
14
+
15
+ def force_local_lib_override(project_name = @project_name)
16
+ rakefile = File.read(File.join(project_name, 'Rakefile'))
17
+ File.open(File.join(project_name, 'Rakefile'), "w+") do |f|
18
+ f << "$:.unshift('#{@lib_path}')\n"
19
+ f << rakefile
20
+ end
21
+ end
22
+
23
+ def setup_active_project_folder project_name
24
+ @active_project_folder = File.join(@tmp_root, project_name)
25
+ @project_name = project_name
26
+ end
27
+ end
28
+
29
+ World(CommonHelpers)
@@ -0,0 +1,24 @@
1
+ require File.dirname(__FILE__) + "/../../lib/codefumes"
2
+
3
+ gem 'cucumber'
4
+ require 'cucumber'
5
+ gem 'rspec'
6
+ require 'spec'
7
+ require 'spec/stubs/cucumber'
8
+
9
+ include CodeFumes
10
+
11
+ gem 'aruba'
12
+ require 'aruba'
13
+
14
+ Before do
15
+ @tmp_root = File.dirname(__FILE__) + "/../../tmp"
16
+ @home_path = File.expand_path(File.join(@tmp_root, "home"))
17
+ @lib_path = File.expand_path(File.dirname(__FILE__) + "/../../lib")
18
+ @bin_path = File.expand_path(File.dirname(__FILE__) + "/../../bin")
19
+ FileUtils.rm_rf @tmp_root
20
+ FileUtils.mkdir_p @home_path
21
+ ENV['HOME'] = @home_path
22
+ ENV['CODEFUMES_CONFIG_FILE'] = File.expand_path(File.join(@tmp_root, "codefumes_config_file"))
23
+ ENV['FUMES_ENV'] = ENV['FUMES_ENV'] || "test"
24
+ end
@@ -0,0 +1,11 @@
1
+ module Matchers
2
+ def contain(expected)
3
+ simple_matcher("contain #{expected.inspect}") do |given, matcher|
4
+ matcher.failure_message = "expected #{given.inspect} to contain #{expected.inspect}"
5
+ matcher.negative_failure_message = "expected #{given.inspect} not to contain #{expected.inspect}"
6
+ given.index expected
7
+ end
8
+ end
9
+ end
10
+
11
+ World(Matchers)
@@ -0,0 +1,33 @@
1
+ Feature: Synchronizing a repository with CodeFumes
2
+ Keeping a CodeFumes project synchronized with a project's development
3
+ is a fundamental feature of the site/service. Synchronizing this data must
4
+ be as simple, quick, and reliable as possible in order to provide value to
5
+ the users of the site.
6
+
7
+ Scenario: Unsupported repository type
8
+ When I run "#{@bin_path}/fumes sync"
9
+ Then it should fail with:
10
+ """
11
+ Unsupported
12
+ """
13
+ And the exit status should be 1
14
+
15
+ Scenario: Successful synchronization
16
+ Given I have cloned 1 project
17
+ When I synchronize the project
18
+ Then the exit status should be 0
19
+ And the output should contain "Successfully saved"
20
+ And the output should contain "Visit http://"
21
+
22
+ Scenario: Providing feedback when data is being sent to a non-production server
23
+ Given I have cloned 1 project
24
+ When I synchronize the project
25
+ Then the output should contain "non-production"
26
+ And the output should contain "test.codefumes.com"
27
+ And the exit status should be 0
28
+
29
+ Scenario: Specifying a custom, but non-existant public/private key combination
30
+ Given I have cloned 1 project
31
+ And I cd to "project_1/"
32
+ When I run "#{@bin_path}/fumes sync -p non-existant-pubkey -a non-existant-privkey"
33
+ And the exit status should be 2
data/lib/codefumes.rb CHANGED
@@ -1,14 +1,16 @@
1
- require 'httparty'
2
- require 'chronic'
1
+ require 'grit'
3
2
 
4
3
  require 'codefumes/api'
5
- require 'codefumes/build'
6
- require 'codefumes/claim'
7
- require 'codefumes/commit'
8
4
  require 'codefumes/config_file'
9
- require 'codefumes/payload'
10
- require 'codefumes/project'
5
+ require 'codefumes/errors'
6
+ require 'codefumes/exit_codes'
7
+ require 'codefumes/harvester.rb'
8
+ require 'codefumes/quick_build.rb'
9
+ require 'codefumes/quick_metric.rb'
10
+ require 'codefumes/source_control.rb'
11
+
12
+ include CodeFumes::API
11
13
 
12
14
  module CodeFumes
13
- VERSION = '0.1.10' unless defined?(CodeFumes::VERSION)
15
+ VERSION = '0.2.0' unless defined?(CodeFumes::VERSION)
14
16
  end
data/lib/codefumes/api.rb CHANGED
@@ -1,7 +1,16 @@
1
+ require 'httparty'
2
+ require 'chronic'
3
+
4
+ require 'codefumes/api/build'
5
+ require 'codefumes/api/claim'
6
+ require 'codefumes/api/commit'
7
+ require 'codefumes/api/payload'
8
+ require 'codefumes/api/project'
9
+
1
10
  module CodeFumes
2
- class API
11
+ module API
3
12
  include HTTParty
4
-
13
+ base_uri 'http://codefumes.com/api/v1/xml'
5
14
  format :xml
6
15
 
7
16
  BASE_URIS = {
@@ -10,16 +19,16 @@ module CodeFumes
10
19
  :local => 'http://codefumes.com.local/api/v1/xml'
11
20
  } #:nodoc:
12
21
 
13
- # Set the connection base for all server requests. Valid options
14
- # are +:production+ and +:test+, which connect to
15
- # http://codefumes.com and http://test.codefumes.com (respectively).
16
- #
17
- # +:local+ is also technically supported, but provided for local
18
- # testing and likely only useful for CodeFumes.com developers.
19
- def self.mode(mode)
20
- base_uri(BASE_URIS[mode]) if BASE_URIS[mode]
22
+ def self.mode=(mode)
23
+ return if mode.to_s.empty?
24
+ base_uri(BASE_URIS[mode.to_sym]) if BASE_URIS[mode.to_sym]
21
25
  end
22
26
 
23
- mode(:production)
27
+ def self.mode?(mode)
28
+ return false if mode.nil?
29
+ base_uri == BASE_URIS[mode.to_sym]
30
+ end
24
31
  end
25
32
  end
33
+
34
+ CodeFumes::API.mode= ENV['FUMES_ENV']
@@ -0,0 +1,139 @@
1
+ module CodeFumes
2
+ module API
3
+ # Represents a specific instance of tests running on
4
+ # a continuous integration server. Builds have a name and are
5
+ # associated with # a specific Commit of a Project and can track
6
+ # the current status (running, failed, success) and the
7
+ # start & end times of the Build process.
8
+ class Build
9
+ attr_reader :created_at, :api_uri, :identifier, :commit, :project
10
+ attr_accessor :started_at, :ended_at, :state, :name
11
+
12
+ # Initializes new instance of a Build.
13
+ #
14
+ # * commit - Instance of CodeFumes::Commit to associate the Build with
15
+ # * name - A name for the build ('ie7', 'specs', etc.)
16
+ # * state - Current state of the build (defaults: 'running')
17
+ # * valid values: 'running', 'failed', 'successful'
18
+ # * options - Hash of additional options. Accepts the following:
19
+ # * :started_at - Time the build started
20
+ # * :ended_at - Time the build completed (defaults to nil)
21
+ def initialize(commit, name, state = 'running', options = {})
22
+ @commit = commit
23
+ @project = commit.project
24
+ @name = name
25
+ @state = state.to_s
26
+ @started_at = options[:started_at] || options['started_at'] || Time.now
27
+ @ended_at = options[:ended_at] || options['ended_at']
28
+ end
29
+
30
+ # Overrides existing attributes with those supplied in +options+. This
31
+ # simplifies the process of updating an object's state when given a response
32
+ # from the CodeFumes API.
33
+ #
34
+ # Valid options are:
35
+ # * identifier
36
+ # * name
37
+ # * state
38
+ # * started_at
39
+ # * ended_at
40
+ # * api_uri
41
+ #
42
+ # Returns +self+
43
+ def reinitialize_from_hash!(options = {})
44
+ @identifier = options[:identifier] || options['identifier']
45
+ @name = options[:name] || options['name']
46
+ @state = options[:state] || options['state']
47
+ @api_uri = options[:api_uri] || options['api_uri']
48
+ @started_at = options[:started_at] || options['started_at']
49
+ @ended_at = options[:ended_at] || options['ended_at']
50
+ @created_at = options[:created_at] || options['created_at']
51
+ self
52
+ end
53
+
54
+ # Saves the Build instance to CodeFumes.com
55
+ #
56
+ # Returns +true+ if successful
57
+ #
58
+ # Returns +false+ if request fails
59
+ # ---
60
+ # TODO: Make this consistent w/ other class' create/update handling
61
+ def save
62
+ response = exists? ? update : create
63
+
64
+ case response.code
65
+ when 201,200
66
+ reinitialize_from_hash!(response['build'])
67
+ true
68
+ else
69
+ false
70
+ end
71
+ end
72
+
73
+ # Searches website for Build with the supplied identifier.
74
+ #
75
+ # Returns a Build instance if the Build exists and is available,
76
+ # to the user making the request.
77
+ #
78
+ # Returns +nil+ in all other cases.
79
+ def self.find(commit, build_name)
80
+ project = commit.project
81
+ uri = "/projects/#{project.public_key}/commits/#{commit.identifier}/builds/#{build_name}"
82
+
83
+ response = API.get(uri)
84
+
85
+ case response.code
86
+ when 200
87
+ build_params = response["build"] || {}
88
+ name = build_params["name"]
89
+ state = build_params["state"]
90
+ build = Build.new(commit, name, state)
91
+ build.reinitialize_from_hash!(build_params)
92
+ else
93
+ nil
94
+ end
95
+ end
96
+
97
+
98
+ # Returns true if the request was successful
99
+ #
100
+ # Returns +false+ in all other cases.
101
+ def destroy
102
+ uri = "/projects/#{@project.public_key}/commits/#{@commit.identifier}/builds/#{@name}"
103
+ auth_args = {:username => @project.public_key, :password => @project.private_key}
104
+
105
+ response = API.delete(uri, :basic_auth => auth_args)
106
+
107
+ case response.code
108
+ when 200 : true
109
+ else false
110
+ end
111
+ end
112
+
113
+ private
114
+ # Verifies existence of Build on website.
115
+ #
116
+ # Returns +true+ if a build with the specified identifier or name is associated with
117
+ # the specified project/commit
118
+ #
119
+ # Returns +false+ if the public key of the Project is not available.
120
+ def exists?
121
+ !self.class.find(commit, name).nil?
122
+ end
123
+
124
+ # Saves a new build (makes POST request)
125
+ def create
126
+ API.post("/projects/#{project.public_key}/commits/#{commit.identifier}/builds", :query => {:build => standard_content_hash}, :basic_auth => {:username => project.public_key, :password => project.private_key})
127
+ end
128
+
129
+ # Updates an existing build (makes PUT request)
130
+ def update
131
+ API.put("/projects/#{project.public_key}/commits/#{commit.identifier}/builds/#{name}", :query => {:build => standard_content_hash}, :basic_auth => {:username => project.public_key, :password => project.private_key})
132
+ end
133
+
134
+ def standard_content_hash
135
+ {:name => name,:started_at => started_at, :ended_at => ended_at, :state => state}
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,74 @@
1
+ module CodeFumes
2
+ module API
3
+ class Claim
4
+ attr_accessor :created_at
5
+ SUPPORTED_VISIBILITIES = [:public, :private]
6
+
7
+ # Attempts to claim the specified Project instance using the
8
+ # supplied API key.
9
+ #
10
+ # +visibility+ defaults to +:public+. Valid options are +public+
11
+ # and +private+.
12
+ #
13
+ # Similar to Project#claim, but more explicit.
14
+ #
15
+ # Returns +true+ if the request is successful, or if the project
16
+ # was already owned by the user associated with the privided API
17
+ # key.
18
+ #
19
+ # Returns +false+ in all other cases.
20
+ def self.create(project, api_key, visibility = :public)
21
+ validate_api_key(api_key)
22
+ validate_visibility(visibility)
23
+
24
+ auth_args = {:username => project.public_key, :password => project.private_key}
25
+
26
+ uri = "/projects/#{project.public_key}/claim"
27
+ response = API.put(uri, :query => {:api_key => api_key, :visibility => visibility}, :basic_auth => auth_args)
28
+
29
+ case response.code
30
+ when 200 : true
31
+ else false
32
+ end
33
+ end
34
+
35
+ # Removes a claim on the specified Project instance using the
36
+ # supplied API key, releasing ownership. If the project was a
37
+ # "private" project, this method will convert it to "public".
38
+ #
39
+ # Returns true if the request was successful or there was not
40
+ # an existing owner (the action is idempotent).
41
+ #
42
+ # Returns +false+ in all other cases.
43
+ def self.destroy(project, api_key)
44
+ validate_api_key(api_key)
45
+
46
+ auth_args = {:username => project.public_key, :password => project.private_key}
47
+
48
+ uri = "/projects/#{project.public_key}/claim"
49
+ response = API.delete(uri, :query => {:api_key => api_key}, :basic_auth => auth_args)
50
+
51
+ case response.code
52
+ when 200 : true
53
+ else false
54
+ end
55
+ end
56
+
57
+ private
58
+ def self.validate_api_key(api_key)
59
+ if api_key.nil? || api_key.empty?
60
+ msg = "Invalid user api key provided. (provided: '#{api_key}')"
61
+ raise(Errors::NoUserApiKeyError, msg)
62
+ end
63
+ end
64
+
65
+ def self.validate_visibility(visibility)
66
+ unless SUPPORTED_VISIBILITIES.include?(visibility.to_sym)
67
+ msg = "Unsupported visibility supplied (#{visibility.to_s}). "
68
+ msg << "Valid options are: #{SUPPORTED_VISIBILITIES.join(', ')}"
69
+ raise ArgumentError, msg
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,150 @@
1
+ module CodeFumes
2
+ module API
3
+ # Similar to a revision control system, a Commit encompasses a set of
4
+ # changes to a codebase, who made them, when said changes were applied
5
+ # to the previous revision of codebase, et cetera.
6
+ #
7
+ # A Commit has a concept of 'standard attributes' which will always be
8
+ # present in a response from CodeFumes.com[http://codefumes.com], such
9
+ # as the +identifier+, +author+, and +commit_message+ (see the list of
10
+ # attributes for a comprehensive listing). In addition to this, users
11
+ # are able to associate 'custom attributes' to a Commit, allowing
12
+ # users to link any number of attributes with a commit identifier and
13
+ # easily retrieve them later.
14
+ #
15
+ # One thing to note about Commit objects is that they are read-only.
16
+ # To associate metrics with a Commit object, a Payload object should
17
+ # be created and saved. Refer to the Payload documentation for more
18
+ # information.
19
+ class Commit
20
+ attr_reader :identifier, :author_name, :author_email, :committer_name,
21
+ :committer_email, :short_message, :message,:committed_at,
22
+ :authored_at, :uploaded_at, :api_uri, :parent_identifiers,
23
+ :line_additions, :line_deletions, :line_total,
24
+ :affected_file_count, :custom_attributes, :project
25
+ alias_method :id, :identifier
26
+ alias_method :sha, :identifier
27
+
28
+ # Instantiates a new Commit object
29
+ #
30
+ # * +identifier+ - the revision number of the commit (git commit sha, svn version, etc)
31
+ #
32
+ # Accepts a Hash of options, including:
33
+ # * author_email
34
+ # * author_name
35
+ # * committer_email
36
+ # * committer_name
37
+ # * short_message
38
+ # * message
39
+ # * committed_at
40
+ # * authored_at
41
+ # * uploaded_at
42
+ # * api_uri
43
+ # * parent_identifiers
44
+ # * line_additions
45
+ # * line_deletions
46
+ # * line_total
47
+ # * affected_file_count
48
+ # * custom_attributes
49
+ #
50
+ # +custom_attributes+ should be a Hash of attribute_name/value
51
+ # pairs associated with the commit. All other attributes are
52
+ # expected to be String values, other than +committed_at+ and
53
+ # +authored_at+, which are expected to be DateTime objects.
54
+ # Technically speaking, you could pass anything you wanted into
55
+ # the fields, but when using with the CodeFumes API, the attribute
56
+ # values will be of the type String, DateTime, or Hash.
57
+ def initialize(project, identifier, options = {})
58
+ @project = project
59
+ @identifier = identifier
60
+ @author_email = options[:author_email] || options["author_email"]
61
+ @author_name = options[:author_name] || options["author_name"]
62
+ @committer_email = options[:committer_email] || options["committer_email"]
63
+ @committer_name = options[:committer_name] || options["committer_name"]
64
+ @short_message = options[:short_message] || options["short_message"]
65
+ @message = options[:message] || options["message"]
66
+ @committed_at = options[:committed_at] || options["committed_at"]
67
+ @authored_at = options[:authored_at] || options["authored_at"]
68
+ @uploaded_at = options[:uploaded_at] || options["uploaded_at"]
69
+ @api_uri = options[:api_uri] || options["api_uri"]
70
+ @parent_identifiers = options[:parent_identifiers] || options["parent_identifiers"]
71
+ @line_additions = options[:line_additions] || options["line_additions"]
72
+ @line_deletions = options[:line_deletions] || options["line_deletions"]
73
+ @line_total = options[:line_total] || options["line_total"]
74
+ @affected_file_count = options[:affected_file_count] || options["affected_file_count"]
75
+ @custom_attributes = options[:custom_attributes] || options["custom_attributes"] || {}
76
+ convert_custom_attributes_keys_to_symbols
77
+ end
78
+
79
+ # Returns the name of the author and the email associated
80
+ # with the commit in a string formatted as:
81
+ # "Name [email_address]"
82
+ # (ie: "John Doe [jdoe@example.com]")
83
+ def author
84
+ "#{author_name} [#{author_email}]"
85
+ end
86
+
87
+ # Returns the name of the committer and the email associated
88
+ # with the commit in a string formatted as:
89
+ # "Name [email_address]"
90
+ # (ie: "John Doe [jdoe@example.com]")
91
+ def committer
92
+ "#{committer_name} [#{committer_email}]"
93
+ end
94
+
95
+ # Returns the Commit object associated with the supplied identifier.
96
+ # Returns nil if the identifier is not found.
97
+ def self.find(project, identifier)
98
+ response = API.get("/projects/#{project.public_key}/commits/#{identifier}")
99
+ case response.code
100
+ when 200
101
+ return nil if response["commit"].empty?
102
+ new(project, response["commit"].delete("identifier"), response["commit"])
103
+ else
104
+ nil
105
+ end
106
+ end
107
+
108
+ # Returns a collection of commits associated with the specified
109
+ # Project public key.
110
+ def self.all(project)
111
+ response = API.get("/projects/#{project.public_key}/commits")
112
+ case response.code
113
+ when 200
114
+ return [] if response["commits"].empty? || response["commits"]["commit"].nil?
115
+ response["commits"]["commit"].map do |commit_data|
116
+ new(commit_data.delete("identifier"), commit_data)
117
+ end
118
+ else
119
+ nil
120
+ end
121
+ end
122
+
123
+ # Returns the most recent commit associated with the specified
124
+ # Project public key.
125
+ def self.latest(project)
126
+ response = API.get("/projects/#{project.public_key}/commits/latest")
127
+ case response.code
128
+ when 200
129
+ new(project, response["commit"].delete("identifier"), response["commit"])
130
+ else
131
+ nil
132
+ end
133
+ end
134
+
135
+ # Returns the commit identifier of the most recent commit of with
136
+ # the specified Project public key.
137
+ def self.latest_identifier(project)
138
+ latest_commit = latest(project)
139
+ latest_commit.nil? ? nil : latest_commit.identifier
140
+ end
141
+
142
+ private
143
+ def convert_custom_attributes_keys_to_symbols
144
+ @custom_attributes = @custom_attributes.inject({}) do |results, key_and_value|
145
+ results.merge! key_and_value.first.to_sym => key_and_value.last
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end