codefumes 0.1.10 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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