association_accessors 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Appraisals +25 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +254 -0
  7. data/Rakefile +27 -0
  8. data/association_accessors.gemspec +39 -0
  9. data/bin/test +21 -0
  10. data/gemfiles/activerecord_4.1.gemfile +7 -0
  11. data/gemfiles/activerecord_4.1.gemfile.lock +67 -0
  12. data/gemfiles/activerecord_4.2.gemfile +7 -0
  13. data/gemfiles/activerecord_4.2.gemfile.lock +67 -0
  14. data/gemfiles/activerecord_5.0.gemfile +7 -0
  15. data/gemfiles/activerecord_5.0.gemfile.lock +63 -0
  16. data/gemfiles/activerecord_5.1.gemfile +7 -0
  17. data/gemfiles/activerecord_5.1.gemfile.lock +63 -0
  18. data/gemfiles/activerecord_5.2.gemfile +7 -0
  19. data/gemfiles/activerecord_5.2.gemfile.lock +63 -0
  20. data/gemfiles/rspec_2.gemfile +7 -0
  21. data/gemfiles/rspec_2.gemfile.lock +58 -0
  22. data/lib/association_accessors.rb +30 -0
  23. data/lib/association_accessors/collection_association.rb +35 -0
  24. data/lib/association_accessors/singular_association.rb +19 -0
  25. data/lib/association_accessors/test/matcher.rb +60 -0
  26. data/lib/association_accessors/version.rb +3 -0
  27. data/spec/association_accessors_spec.rb +5 -0
  28. data/spec/dummy.rb +99 -0
  29. data/spec/matcher_spec.rb +56 -0
  30. data/spec/spec_helper.rb +23 -0
  31. data/spec/with_association/belongs_to_spec.rb +33 -0
  32. data/spec/with_association/has_and_belongs_to_many_spec.rb +45 -0
  33. data/spec/with_association/has_many_spec.rb +45 -0
  34. data/spec/with_association/has_many_through_spec.rb +27 -0
  35. data/spec/with_association/has_one_spec.rb +33 -0
  36. data/spec/with_association/has_one_through_spec.rb +43 -0
  37. data/spec/with_association/polymorphic/belongs_to_spec.rb +38 -0
  38. data/spec/with_association/polymorphic/has_many_spec.rb +44 -0
  39. data/spec/with_association/polymorphic/has_one_spec.rb +33 -0
  40. data/spec/with_association/with_custom_name_spec.rb +33 -0
  41. data/spec/with_attribute_spec.rb +47 -0
  42. metadata +203 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 427e524e67a28002e3e0b010c7536c410597d3d23bdfbb8dd8375743124b4251
4
+ data.tar.gz: ec24e909df781cdd00d70035451aaec6420617827fa8244531faf2381b09633c
5
+ SHA512:
6
+ metadata.gz: b787c454deb6947cd9f71f8fc7f710024dadebb8587e9ef6da762ca2124b463ef16e03c6b99897f09e28f9489bb3ca2166290d6e63cec81c9b27b8271da9f4d5
7
+ data.tar.gz: dae9c1963ff3c53f2aca2081b41b23e0a22d0e3b05a3ac63ed9363a4ca4631d8291a691880c9bdcd931fd54f77ba093f13f556662a7ec3985f08f35e78b3ca4b
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Appraisals ADDED
@@ -0,0 +1,25 @@
1
+ if RUBY_VERSION < '2.4'
2
+ appraise 'activerecord-4.1' do
3
+ gem 'activerecord', '4.1.7'
4
+ end
5
+ end
6
+
7
+ appraise 'activerecord-4.2' do
8
+ gem 'activerecord', '4.2.0'
9
+ end
10
+
11
+ appraise 'activerecord-5.0' do
12
+ gem 'activerecord', '5.0.3'
13
+ end
14
+
15
+ appraise 'activerecord-5.1' do
16
+ gem 'activerecord', '5.1.5'
17
+ end
18
+
19
+ appraise 'activerecord-5.2' do
20
+ gem 'activerecord', '5.2.2'
21
+ end
22
+
23
+ appraise 'rspec-2' do
24
+ gem 'rspec', '~> 2.0'
25
+ end
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in association_accessors.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Szijjártó Nagy Misu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # AssociationAccessors
2
+
3
+ **association_accessors** is a tool for generating accessors for `ActiveRecord` model associations based on columns other than the `id` (primary key).
4
+
5
+ ## Dependencies
6
+
7
+ * **ruby** `'>=2.3'`
8
+ * **activerecord** `'>=4.1', '<6.0'`
9
+ * **rspec** `'>= 2.0'` (for the test matcher)
10
+
11
+ ## Table of Contents
12
+
13
+ * [AssociationAccessors](#associationaccessors)
14
+ * [Dependencies](#dependencies)
15
+ * [Table of Contents](#table-of-contents)
16
+ * [The Challenge](#the-challenge)
17
+ * [Installation](#installation)
18
+ * [Config](#config)
19
+ * [Usage](#usage)
20
+ * [With belongs_to](#with-belongs_to)
21
+ * [With has_one](#with-has_one)
22
+ * [With has_many](#with-has_many)
23
+ * [With has_and_belongs_to_many](#with-has_and_belongs_to_many)
24
+ * [With custom association names](#with-custom-association-names)
25
+ * [With polymorphic associations](#with-polymorphic-associations)
26
+ * [Test matcher](#test-matcher)
27
+ * [Contributing](#contributing)
28
+ * [Development](#development)
29
+ * [License](#license)
30
+
31
+ ## The Challenge
32
+
33
+ You have the following tables:
34
+
35
+ ```ruby
36
+ ActiveRecord::Schema.define do
37
+ create_table :companies, force: true do |t|
38
+ t.string :serial
39
+ t.string :name
40
+ # ...
41
+ end
42
+
43
+ create_table :users, force: true do |t|
44
+ t.string :serial
45
+ t.integer :company_id
46
+ t.string :name
47
+ # ...
48
+ end
49
+ end
50
+ ```
51
+
52
+ For security reasons it is decided that the frontend must not see the `id`s, only the `serial`s. So, when you want to change the company of a user, `params` will contain something like this:
53
+
54
+ ```ruby
55
+ # <ActionController::Parameters { "serial": "9jco5RMp4K", "user": { company_serial: "MbyDB18lCi" } } permitted: false>
56
+ ```
57
+
58
+ Now, if the `User` model has `#company_serial=` method, you can simply permit `:company_serial`.
59
+
60
+ Using **association_accessors** it amounts to this:
61
+
62
+ ```ruby
63
+ class User < ActiveRecord::Base
64
+ include AssociationAccessors
65
+
66
+ belongs_to :company
67
+ association_accessor_for :company, with_attribute: :serial
68
+ end
69
+ ```
70
+
71
+ ## Installation
72
+
73
+ Add this line to your application's Gemfile:
74
+
75
+ ```ruby
76
+ gem 'association_accessors'
77
+ ```
78
+
79
+ And then execute:
80
+
81
+ $ bundle
82
+
83
+ ## Config
84
+
85
+ It is possible to set a default value for the keyword `with_attribute:`, for example in an initializer:
86
+
87
+ ```ruby
88
+ # config/initializers/association_accessors.rb
89
+ AssociationAccessors.default_attribute = :serial
90
+ ```
91
+
92
+ ## Usage
93
+
94
+ 1. include the module `AssociationAccessors`.
95
+ 2. define the associations
96
+ 3. call `.association_accessor_for` method with the association name and the identifier attribute to generate the accessor methods:
97
+ * for singular associations it will generate a reader and a writer along the rule `[association_name]_[attribute_name]`
98
+ * for collection associations it will generate the methods along the rule `[singular_association_name]_[plural_attribute_name]`
99
+
100
+ These accessors work more or less the same way as the `id` accessors generated by default for the associations.
101
+
102
+ There are a few exceptions and some gotchas, though.
103
+
104
+ ### With `belongs_to`
105
+
106
+ ```ruby
107
+ # AssociationAccessors is included in the ApplicationRecord
108
+ class User < ApplicationRecord
109
+ belongs_to :company, optional: true # only to demonstrate that it works with the association `nil`
110
+ association_accessor_for :company, with_attribute: :serial
111
+ end
112
+ ```
113
+
114
+ You have the methods `#company_id` and `#company_id=` because of the foreign key column. `association_accessors` generates the methods `#company_serial` and `#company_serial=`.
115
+
116
+ These work in a similar way with the following differences:
117
+ * `#company_id` is a column, `#company_serial` is a computed value (the `serial` of the `company`) - where `#company_id` can return value other than `nil` even without really having a `company`, `#company_serial` can only return the `serial` of the `company` (if any)
118
+ * `#company_id=` does not break if there is no company with the given id, only `#company` will return `nil` - whereas calling `#company_serial=` with a not existing serial will raise `ActiveRecord::RecordNotFound`
119
+
120
+ ```ruby
121
+ user.company # => #<Company id: 10, serial: "HVuPpK">
122
+ user.company_id # => 10
123
+ user.company_serial # => "HVuPpK"
124
+
125
+ user.company_id = 100 # there is no company with id=100
126
+ user.company # => nil
127
+ user.company_id # => 100
128
+ user.company_serial # => nil
129
+
130
+ user.company_serial = 'EeNRM'
131
+ # ActiveRecord::RecordNotFound (Couldn't find Company)
132
+ ```
133
+
134
+ ### With `has_one`
135
+
136
+ ```ruby
137
+ class User < ApplicationRecord
138
+ has_one :address
139
+ association_accessor_for :address, with_attribute: :serial
140
+ end
141
+ ```
142
+
143
+ There is no `#address_id` or `#address_id=` method for a `has_one` association. The generated methods (`#address_serial`, `#address_serial=`) behave the following way:
144
+ * `#address_serial` returns the `serial` of the `address` if any
145
+ * `#address_serial=`
146
+ * changes the `user_id` column of the `adress`(es) setting it to `null` or to the `id` of the user as required
147
+ * raises `ActiveRecord::RecordNotFound` if the given `serial` does not exist
148
+ * raises `ActiveRecord::HasOneThroughCantAssociateThroughHasOneOrManyReflection` if association is defined with `through:` option
149
+
150
+ ### With `has_many`
151
+
152
+ ```ruby
153
+ class Company < ApplicationRecord
154
+ has_many :users
155
+ association_accessor_for :users, with_attribute: :serial
156
+ end
157
+ ```
158
+
159
+ The generated methods (`#user_serials` and `#user_serials=`) will behave exactly the same way the [`collection_singular_ids`](https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-many) accessor generated by `rails`:
160
+ * `#user_ids` will return the `id`s of the `users` - `#user_serials` will return the `serial`s of the `users`
161
+ * `#user_ids=` and `#user_serials=` will both
162
+ * update the `company_id` column of the users: setting it to `null` or to the `id` of the company as required
163
+ * raise `ActiveRecord::RecordNotFound` if any of the given `id`s or `serial`s is non-existing
164
+ * raise `ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection` if association is defined with `:through` option
165
+
166
+ ### With `has_and_belongs_to_many`
167
+
168
+ ```ruby
169
+ class User < ApplicationRecord
170
+ has_and_belongs_to_many :tasks
171
+ association_accessor_for :tasks, with_attribute: :serial
172
+ end
173
+ ```
174
+
175
+ The generated methods (`#task_serials` and `#task_serials=`) will behave exactly the same way the [`collection_singular_ids`](https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-and-belongs-to-many) accessor generated by `rails`:
176
+ * `#task_ids` will return the `id`s of the `tasks` - `#task_serials` will return the `serial`s of the `tasks`
177
+ * `#task_ids=` and `#task_serials=` will both
178
+ * update the join table adding or deleting records as required
179
+ * raise `ActiveRecord::RecordNotFound` if any of the given `id`s or `serial`s is non-existing
180
+
181
+ ### With custom association names
182
+
183
+ ```ruby
184
+ class Node < ApplicationRecord
185
+ belongs_to :parent, class_name: 'Node'
186
+ association_accessor_for :parent, with_attribute: :serial
187
+
188
+ has_many :children, class_name: 'Node', foreign_key: :parent_id
189
+ association_accessor_for :children, with_attribute: :serial
190
+ end
191
+ ```
192
+
193
+ The generated methods (`#parent_serial`, `#parent_serial=`, `#child_serials` and `#child_serials=`) work the same way, as they rely on the association, not the column.
194
+
195
+ ### With polymorphic associations
196
+
197
+ ```ruby
198
+ class User < ApplicationRecord
199
+ has_one :image, as: :imageable
200
+ association_accessor_for :image, with_attribute: :serial
201
+ end
202
+
203
+ class Task < ApplicationRecord
204
+ has_many :images, as: :imageable
205
+ association_accessor_for :images, with_attribute: :serial
206
+ end
207
+
208
+ class Image < ApplicationRecord
209
+ belongs_to :imageable, polymorphic: true
210
+ end
211
+ ```
212
+
213
+ * The `has_one` and `has_many` parts work just fine, setting the `imageable_type` and `imageable_id` to `nil` or the class name and the `id` of the required record
214
+ * It is **not** recommended to use `association_accessors` for the `belongs_to` part of polymorphic associations, as the associated class is derived from the actual value of `[association_name]_type` column, causing unpredictable behavior and raising `NoMethodError` when it is `nil`
215
+
216
+ ## Test matcher
217
+
218
+ **association_accessors** ships a test matcher too: `#have_association_accessor_for`. It surely works with [rspec](https://relishapp.com/rspec).
219
+
220
+ ```ruby
221
+ RSpec.configure do |config|
222
+ config.include AssociationAccessors::Test, type: :model
223
+ end
224
+
225
+ RSpec.describe User, type: :model do
226
+ it { should have_association_accessor_for(:company).with_attribute(:serial) }
227
+ end
228
+ ```
229
+
230
+ So far, it checks if the reader and writer methods are defined on the subject, nothing more.
231
+
232
+ ## Contributing
233
+
234
+ Bug reports and pull requests are welcome on GitHub at https://github.com/SzNagyMisu/association_accessors.
235
+
236
+ ## Development
237
+
238
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
239
+
240
+ To test against other `activerecord` or `rspec` versions, check the `./Appraisals` file and choose one:
241
+
242
+ $ bundle exec appraisal activerecord-5.0 bin/console
243
+ $ bundle exec appraisal activerecord-5.0 rspec
244
+
245
+ To run a full test suite (including multiple ruby, activerecord and rspec versions), run:
246
+
247
+ $ rake full_test
248
+ $ RUBY_VERSIONS=2.5.3,2.6.1 rake full_test
249
+
250
+ Setting the environment variable `RUBY_VERSIONS` overrides the default (`2.3.7`, `2.4.1`, `2.5.1`) ruby versions to test against. Note that while the ruby versions must be installed for the test to run, the gems are handled by [appraisal](https://github.com/thoughtbot/appraisal).
251
+
252
+ ## License
253
+
254
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ DEFAULT_RUBY_VERSIONS = [ '2.3.7', '2.4.1', '2.5.1' ].freeze
5
+
6
+ desc "Test against multiple ruby, activerecord and rspec versions - " +
7
+ "set RUBY_VERSIONS to the version(s) to test with (default: #{DEFAULT_RUBY_VERSIONS.join(',').inspect})"
8
+ task :full_test do
9
+ versions = ENV['RUBY_VERSIONS'] ? ENV['RUBY_VERSIONS'].split(',') : DEFAULT_RUBY_VERSIONS
10
+ started_at = Time.now
11
+
12
+ versions.each do |version|
13
+ sh "./bin/test #{version}"
14
+ end
15
+
16
+ time_elapsed = (Time.now - started_at).to_i
17
+ hours = time_elapsed / 3600
18
+ minutes = time_elapsed / 60 % 60
19
+ seconds = time_elapsed % 60
20
+ puts "\nTest suit complete\n" +
21
+ " ruby versions: #{versions.join(', ')}\n" +
22
+ " run in #{hours}h #{minutes}m #{seconds}s\n"
23
+ end
24
+
25
+ RSpec::Core::RakeTask.new(:spec)
26
+
27
+ task :default => :spec
@@ -0,0 +1,39 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "association_accessors/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "association_accessors"
8
+ spec.version = AssociationAccessors::VERSION
9
+ spec.authors = ["Szijjártó Nagy Misu"]
10
+ spec.email = ["szijjartonagy.misu@gmail.com"]
11
+
12
+ spec.summary = 'Handling AR associations with other attributes than primary key'
13
+ spec.description = 'Provides association accessors like `Company#user_serials` and `User#company_serial` where `Company.has_many :users` and `User.belongs_to :company`.'
14
+ spec.homepage = 'https://github.com/SzNagyMisu/association_accessors'
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ else
23
+ raise "RubyGems 2.0 or newer is required to protect against " \
24
+ "public gem pushes."
25
+ end
26
+
27
+ spec.files = `git ls-files -- lib/*`.split("\n") + %w[ README.md LICENSE.txt association_accessors.gemspec ]
28
+ spec.test_files = `git ls-files -- gemfiles/* spec/*`.split("\n") + %w[ Appraisals Gemfile Rakefile .rspec bin/test ]
29
+ spec.bindir = "bin"
30
+ spec.executables = []
31
+ spec.require_paths = ["lib"]
32
+
33
+ spec.add_development_dependency "bundler", "~> 1.16"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ spec.add_development_dependency "activerecord", "~> 5.2"
37
+ spec.add_development_dependency "sqlite3", "~> 1.3"
38
+ spec.add_development_dependency "appraisal", "~> 2.2"
39
+ end
data/bin/test ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ if [ $1 ]; then
6
+ # see https://rvm.io/workflow/scripting
7
+ if [[ -s "$HOME/.rvm/scripts/rvm" ]] ; then
8
+ source "$HOME/.rvm/scripts/rvm"
9
+ elif [[ -s "/usr/local/rvm/scripts/rvm" ]] ; then
10
+ source "/usr/local/rvm/scripts/rvm"
11
+ else
12
+ printf "ERROR: An RVM installation was not found."
13
+ fi
14
+ rvm use $1
15
+ fi
16
+
17
+ bundle install
18
+ bundle exec appraisal install
19
+ bundle exec appraisal rspec
20
+
21
+ echo 'Success!'
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "4.1.7"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,67 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ association_accessors (1.3.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ activemodel (4.1.7)
10
+ activesupport (= 4.1.7)
11
+ builder (~> 3.1)
12
+ activerecord (4.1.7)
13
+ activemodel (= 4.1.7)
14
+ activesupport (= 4.1.7)
15
+ arel (~> 5.0.0)
16
+ activesupport (4.1.7)
17
+ i18n (~> 0.6, >= 0.6.9)
18
+ json (~> 1.7, >= 1.7.7)
19
+ minitest (~> 5.1)
20
+ thread_safe (~> 0.1)
21
+ tzinfo (~> 1.1)
22
+ appraisal (2.2.0)
23
+ bundler
24
+ rake
25
+ thor (>= 0.14.0)
26
+ arel (5.0.1.20140414130214)
27
+ builder (3.2.3)
28
+ concurrent-ruby (1.1.4)
29
+ diff-lcs (1.3)
30
+ i18n (0.9.5)
31
+ concurrent-ruby (~> 1.0)
32
+ json (1.8.6)
33
+ minitest (5.11.3)
34
+ rake (10.5.0)
35
+ rspec (3.8.0)
36
+ rspec-core (~> 3.8.0)
37
+ rspec-expectations (~> 3.8.0)
38
+ rspec-mocks (~> 3.8.0)
39
+ rspec-core (3.8.0)
40
+ rspec-support (~> 3.8.0)
41
+ rspec-expectations (3.8.2)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.8.0)
44
+ rspec-mocks (3.8.0)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.8.0)
47
+ rspec-support (3.8.0)
48
+ sqlite3 (1.3.13)
49
+ thor (0.20.3)
50
+ thread_safe (0.3.6)
51
+ tzinfo (1.2.5)
52
+ thread_safe (~> 0.1)
53
+
54
+ PLATFORMS
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ activerecord (= 4.1.7)
59
+ appraisal (~> 2.2)
60
+ association_accessors!
61
+ bundler (~> 1.16)
62
+ rake (~> 10.0)
63
+ rspec (~> 3.0)
64
+ sqlite3 (~> 1.3)
65
+
66
+ BUNDLED WITH
67
+ 1.16.6