measured 2.8.2 → 3.0.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +11 -5
  3. data/.github/workflows/cla.yml +23 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +8 -0
  6. data/Gemfile +2 -0
  7. data/README.md +114 -4
  8. data/dev.yml +1 -2
  9. data/gemfiles/{activesupport-6.0.gemfile → rails-6.0.gemfile} +1 -0
  10. data/gemfiles/{activesupport-6.1.gemfile → rails-6.1.gemfile} +1 -0
  11. data/gemfiles/rails-7.0.gemfile +6 -0
  12. data/gemfiles/rails-edge.gemfile +6 -0
  13. data/lib/measured/measurable.rb +3 -1
  14. data/lib/measured/rails/active_record.rb +130 -0
  15. data/lib/measured/rails/validations.rb +68 -0
  16. data/lib/measured/railtie.rb +12 -0
  17. data/lib/measured/units/weight.rb +1 -1
  18. data/lib/measured/version.rb +1 -1
  19. data/lib/measured.rb +2 -0
  20. data/lib/tapioca/dsl/compilers/measured_rails.rb +110 -0
  21. data/measured.gemspec +5 -0
  22. data/test/internal/app/models/thing.rb +14 -0
  23. data/test/internal/app/models/thing_with_custom_unit_accessor.rb +18 -0
  24. data/test/internal/app/models/thing_with_custom_value_accessor.rb +19 -0
  25. data/test/internal/app/models/validated_thing.rb +45 -0
  26. data/test/internal/config/database.yml +3 -0
  27. data/test/internal/config.ru +9 -0
  28. data/test/internal/db/.gitignore +1 -0
  29. data/test/internal/db/schema.rb +99 -0
  30. data/test/internal/log/.gitignore +1 -0
  31. data/test/measurable_test.rb +4 -0
  32. data/test/rails/active_record_test.rb +433 -0
  33. data/test/rails/validation_test.rb +252 -0
  34. data/test/tapioca/dsl/compilers/measured_rails_test.rb +220 -0
  35. data/test/test_helper.rb +15 -0
  36. data/test/units/weight_test.rb +3 -1
  37. metadata +80 -7
  38. data/gemfiles/activesupport-5.2.gemfile +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 808003a2689c157f3d3a1ef7ff2eb2f1decc242d23c40a536d9bca8c57364a79
4
- data.tar.gz: 42f4a72969b5331d7cd460f87d5668b3b331001b82f351c221f7153488859be5
3
+ metadata.gz: b794b67ca5a9cc00444ec39526340e3f3710dc0395b7205aeb95f44d8ed698fe
4
+ data.tar.gz: 2696f28b014ce76cded21150a1e1b4ef4772ef92a358ddf4e99b526081125d2a
5
5
  SHA512:
6
- metadata.gz: 4019bd9f128308af57f5ce9440e6a53f05587c1589f120b711c937b70d422a865c14d2e678d4b55bb13054e24a2663b5cb6ef51bfad12e834027eb4a644d5651
7
- data.tar.gz: caf36d6ad0dbbd0e187adbb0d5a2ddd2718a38ddd67920e628c1d7ad09423f9c35edcef7f05cb863fec8e2849bb0eb8ce4145496aa0749cf2aaafbcd6459ba13
6
+ metadata.gz: 95c82144964349b7111b6a1d372384dc3721b3c5e79870c8b841b7468d6794f60702c2acb1be714fab6c5a858a26d0bc28c340ef987c61a5d94658ffd64eec25
7
+ data.tar.gz: 49b6f68fd80cce3886824df3298c2d837ec0f257b0e4bb311231558b0d9a81eb9cb744f3746540eeaa740f6ad1f84bb578441fc6914841ac81ae57005638a1c0
@@ -10,14 +10,20 @@ jobs:
10
10
  strategy:
11
11
  matrix:
12
12
  ruby:
13
- - '2.6'
14
- - '2.7'
15
13
  - '3.0'
14
+ - '3.1'
15
+ - '3.2'
16
16
  gemfile:
17
17
  - Gemfile
18
- - gemfiles/activesupport-5.2.gemfile
19
- - gemfiles/activesupport-6.0.gemfile
20
- - gemfiles/activesupport-6.1.gemfile
18
+ - gemfiles/rails-6.0.gemfile
19
+ - gemfiles/rails-6.1.gemfile
20
+ - gemfiles/rails-7.0.gemfile
21
+ - gemfiles/rails-edge.gemfile
22
+ exclude:
23
+ # Rails Edge only supports Ruby >= 3.1
24
+ - ruby: '3.0'
25
+ gemfile: gemfiles/rails-edge.gemfile
26
+
21
27
  name: Ruby ${{ matrix.ruby }} ${{ matrix.gemfile }}
22
28
  steps:
23
29
  - uses: actions/checkout@v1
@@ -0,0 +1,23 @@
1
+ # .github/workflows/cla.yml
2
+ name: Contributor License Agreement (CLA)
3
+
4
+ on:
5
+ pull_request_target:
6
+ types: [opened, synchronize]
7
+ issue_comment:
8
+ types: [created]
9
+
10
+ jobs:
11
+ cla:
12
+ runs-on: ubuntu-latest
13
+ if: |
14
+ (github.event.issue.pull_request
15
+ && !github.event.issue.pull_request.merged_at
16
+ && contains(github.event.comment.body, 'signed')
17
+ )
18
+ || (github.event.pull_request && !github.event.pull_request.merged)
19
+ steps:
20
+ - uses: Shopify/shopify-cla-action@v1
21
+ with:
22
+ github-token: ${{ secrets.GITHUB_TOKEN }}
23
+ cla-token: ${{ secrets.CLA_TOKEN }}
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.2
data/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@ Unreleased
3
3
 
4
4
 
5
5
 
6
+ 3.0.0
7
+ -----
8
+
9
+ * Merge functionality of `measured-rails` into this gem. From this version on, this gem is able to automatically integrate with Active Record out of the box. (@paracycle)
10
+ * Add `:gm` and `:gms` as aliases to weight. (@kushagra-03)
11
+ * Adds support for initializing `Measured` objects with a rational value from string. (@dvisockas)
12
+ * Make `Measured` initialization faster by avoiding string substitution in certain cases. (@bitwise-aiden)
13
+
6
14
  2.8.2
7
15
  -----
8
16
 
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
+
5
+ gem "activerecord"
data/README.md CHANGED
@@ -4,7 +4,7 @@ Encapsulates measurements with their units. Provides easy conversion between uni
4
4
 
5
5
  Lightweight and easily extensible to include other units and conversions. Conversions done with `Rational` for precision.
6
6
 
7
- The adapter to integrate `measured` with Ruby on Rails is in a separate [`measured-rails`](https://github.com/Shopify/measured-rails) gem.
7
+ Since version 3.0.0, the adapter to integrate `measured` with Ruby on Rails is also a part of this gem. If you had been using [`measured-rails`](https://github.com/Shopify/measured-rails) for that functionality, you should now remove `measured-rails` from your gem file.
8
8
 
9
9
  ## Installation
10
10
 
@@ -158,6 +158,116 @@ Measured::Weight.new("3.14", "kg").format(with_conversion_string: false)
158
158
  > "3.14 kg"
159
159
  ```
160
160
 
161
+ ### Active Record
162
+
163
+ This gem also provides an Active Record adapter for persisting and retrieving measurements with their units, and model validations.
164
+
165
+ Columns are expected to have the `_value` and `_unit` suffix, and be `DECIMAL` and `VARCHAR`, and defaults are accepted. Customizing the column used to hold units is supported, see below for details.
166
+
167
+ ```ruby
168
+ class AddWeightAndLengthToThings < ActiveRecord::Migration
169
+ def change
170
+ add_column :things, :minimum_weight_value, :decimal, precision: 10, scale: 2
171
+ add_column :things, :minimum_weight_unit, :string, limit: 12
172
+
173
+ add_column :things, :total_length_value, :decimal, precision: 10, scale: 2, default: 0
174
+ add_column :things, :total_length_unit, :string, limit: 12, default: "cm"
175
+ end
176
+ end
177
+ ```
178
+
179
+ A column can be declared as a measurement with its measurement subclass:
180
+
181
+ ```ruby
182
+ class Thing < ActiveRecord::Base
183
+ measured Measured::Weight, :minimum_weight
184
+ measured Measured::Length, :total_length
185
+ measured Measured::Volume, :total_volume
186
+ end
187
+ ```
188
+
189
+ You can optionally customize the model's unit column by specifying it in the `unit_field_name` option, as follows:
190
+
191
+ ```ruby
192
+ class ThingWithCustomUnitAccessor < ActiveRecord::Base
193
+ measured_length :length, :width, :height, unit_field_name: :size_unit
194
+ measured_weight :total_weight, :extra_weight, unit_field_name: :weight_unit
195
+ measured_volume :total_volume, :extra_volume, unit_field_name: :volume_unit
196
+ end
197
+ ```
198
+
199
+ Similarly, you can optionally customize the model's value column by specifying it in the `value_field_name` option, as follows:
200
+
201
+ ```ruby
202
+ class ThingWithCustomValueAccessor < ActiveRecord::Base
203
+ measured_length :length, value_field_name: :custom_length
204
+ measured_weight :total_weight, value_field_name: :custom_weight
205
+ measured_volume :volume, value_field_name: :custom_volume
206
+ end
207
+ ```
208
+
209
+ There are some simpler methods for predefined types:
210
+
211
+ ```ruby
212
+ class Thing < ActiveRecord::Base
213
+ measured_weight :minimum_weight
214
+ measured_length :total_length
215
+ measured_volume :total_volume
216
+ end
217
+ ```
218
+
219
+ This will allow you to access and assign a measurement object:
220
+
221
+ ```ruby
222
+ thing = Thing.new
223
+ thing.minimum_weight = Measured::Weight.new(10, "g")
224
+ thing.minimum_weight_unit # "g"
225
+ thing.minimum_weight_value # 10
226
+ ```
227
+
228
+ Order of assignment does not matter, and each property can be assigned separately and with mass assignment:
229
+
230
+ ```ruby
231
+ params = { total_length_unit: "cm", total_length_value: "3" }
232
+ thing = Thing.new(params)
233
+ thing.total_length.to_s # 3 cm
234
+ ```
235
+
236
+ ### Validations
237
+
238
+ Validations are available:
239
+
240
+ ```ruby
241
+ class Thing < ActiveRecord::Base
242
+ measured_length :total_length
243
+
244
+ validates :total_length, measured: true
245
+ end
246
+ ```
247
+
248
+ This will validate that the unit is defined on the measurement, and that there is a value.
249
+
250
+ Rather than `true` the validation can accept a hash with the following options:
251
+
252
+ * `message`: Override the default "is invalid" message.
253
+ * `units`: A subset of units available for this measurement. Units must be in existing measurement.
254
+ * `greater_than`
255
+ * `greater_than_or_equal_to`
256
+ * `equal_to`
257
+ * `less_than`
258
+ * `less_than_or_equal_to`
259
+
260
+ All comparison validations require `Measured::Measurable` values, not scalars. Most of these options replace the `numericality` validator which compares the measurement/method name/proc to the column's value. Validations can also be combined with `presence` validator.
261
+
262
+ **Note:** Validations are strongly recommended since assigning an invalid unit will cause the measurement to return `nil`, even if there is a value:
263
+
264
+ ```ruby
265
+ thing = Thing.new
266
+ thing.total_length_value = 1
267
+ thing.total_length_unit = "invalid"
268
+ thing.total_length # nil
269
+ ```
270
+
161
271
  ## Units and conversions
162
272
 
163
273
  ### SI units support
@@ -269,7 +379,7 @@ Existing alternatives which were considered:
269
379
  * **Cons**
270
380
  * Opens up and modifies `Array`, `Date`, `Fixnum`, `Math`, `Numeric`, `String`, `Time`, and `Object`, then depends on those changes internally.
271
381
  * Lots of code to solve a relatively simple problem.
272
- * No ActiveRecord adapter.
382
+ * No Active Record adapter.
273
383
 
274
384
  ### Gem: [quantified](https://github.com/Shopify/quantified)
275
385
  * **Pros**
@@ -278,7 +388,7 @@ Existing alternatives which were considered:
278
388
  * All math done with floats making it highly lossy.
279
389
  * All units assumed to be pluralized, meaning using unit abbreviations is not possible.
280
390
  * Not actively maintained.
281
- * No ActiveRecord adapter.
391
+ * No Active Record adapter.
282
392
 
283
393
  ### Gem: [unitwise](https://github.com/joshwlewis/unitwise)
284
394
  * **Pros**
@@ -287,7 +397,7 @@ Existing alternatives which were considered:
287
397
  * **Cons**
288
398
  * Lots of code. Good code, but lots of it.
289
399
  * Many modifications to core types.
290
- * ActiveRecord adapter exists but is written and maintained by a different person/org.
400
+ * Active Record adapter exists but is written and maintained by a different person/org.
291
401
  * Not actively maintained.
292
402
 
293
403
  ## Contributing
data/dev.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  name: measured
2
2
 
3
3
  up:
4
- - ruby:
5
- version: 3.0.2
4
+ - ruby
6
5
  - bundler
7
6
 
8
7
  commands:
@@ -3,3 +3,4 @@ source 'https://rubygems.org'
3
3
  gemspec path: '..'
4
4
 
5
5
  gem 'activesupport', '~> 6.0'
6
+ gem "activerecord", '~> 6.0'
@@ -3,3 +3,4 @@ source 'https://rubygems.org'
3
3
  gemspec path: '..'
4
4
 
5
5
  gem 'activesupport', '~> 6.1'
6
+ gem "activerecord", '~> 6.1'
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', '~> 7.0'
6
+ gem 'activerecord', '~> 7.0'
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activesupport', github: 'rails/rails', branch: 'main'
6
+ gem 'activerecord', github: 'rails/rails', branch: 'main'
@@ -17,6 +17,8 @@ class Measured::Measurable < Numeric
17
17
  value
18
18
  when Integer
19
19
  Rational(value)
20
+ when String
21
+ /\d+\/\d+/.match?(value) ? Rational(value) : BigDecimal(value)
20
22
  else
21
23
  BigDecimal(value)
22
24
  end
@@ -30,7 +32,7 @@ class Measured::Measurable < Numeric
30
32
  else
31
33
  value.to_f.to_s
32
34
  end
33
- str.gsub(/\.0*\Z/, "")
35
+ /\.0*\Z/.match?(str) ? str.gsub(/\.0*\Z/, "") : str
34
36
  end.freeze
35
37
  end
36
38
 
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Measured::Rails::ActiveRecord
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def measured(measured_class, *fields)
8
+ options = fields.extract_options!
9
+ options = {}.merge(options)
10
+
11
+ measured_class = measured_class.constantize if measured_class.is_a?(String)
12
+ unless measured_class.is_a?(Class) && measured_class.ancestors.include?(Measured::Measurable)
13
+ raise Measured::Rails::Error, "Expecting #{ measured_class } to be a subclass of Measured::Measurable"
14
+ end
15
+
16
+ options[:class] = measured_class
17
+
18
+ fields.map(&:to_sym).each do |field|
19
+ raise Measured::Rails::Error, "The field #{ field } has already been measured" if measured_fields.key?(field)
20
+
21
+ measured_fields[field] = options
22
+
23
+ unit_field_name = if options[:unit_field_name]
24
+ measured_fields[field][:unit_field_name] = options[:unit_field_name].to_s
25
+ else
26
+ "#{ field }_unit"
27
+ end
28
+
29
+ value_field_name = if options[:value_field_name]
30
+ measured_fields[field][:value_field_name] = options[:value_field_name].to_s
31
+ else
32
+ "#{ field }_value"
33
+ end
34
+
35
+ # Reader to retrieve measured object
36
+ define_method(field) do
37
+ value = public_send(value_field_name)
38
+ unit = public_send(unit_field_name)
39
+
40
+ return nil unless value && unit
41
+
42
+ instance = instance_variable_get("@measured_#{ field }") if instance_variable_defined?("@measured_#{ field }")
43
+ new_instance = begin
44
+ measured_class.new(value, unit)
45
+ rescue Measured::UnitError
46
+ nil
47
+ end
48
+
49
+ if instance == new_instance
50
+ instance
51
+ else
52
+ instance_variable_set("@measured_#{ field }", new_instance)
53
+ end
54
+ end
55
+
56
+ # Writer to assign measured object
57
+ define_method("#{ field }=") do |incoming|
58
+ if incoming.is_a?(measured_class)
59
+ instance_variable_set("@measured_#{ field }", incoming)
60
+ precision = self.column_for_attribute(value_field_name).precision
61
+ scale = self.column_for_attribute(value_field_name).scale
62
+ rounded_to_scale_value = incoming.value.round(scale)
63
+
64
+ max = self.class.measured_fields[field][:max_on_assignment]
65
+ if max && rounded_to_scale_value > max
66
+ rounded_to_scale_value = max
67
+ elsif rounded_to_scale_value.to_i.to_s.length > (precision - scale)
68
+ raise Measured::Rails::Error, "The value #{rounded_to_scale_value} being set for column '#{value_field_name}' has too many significant digits. Please ensure it has no more than #{precision - scale} significant digits."
69
+ end
70
+
71
+ public_send("#{ value_field_name }=", rounded_to_scale_value)
72
+ public_send("#{ unit_field_name }=", incoming.unit.name)
73
+ else
74
+ instance_variable_set("@measured_#{ field }", nil)
75
+ public_send("#{ value_field_name}=", nil)
76
+ public_send("#{ unit_field_name }=", nil)
77
+ end
78
+ end
79
+
80
+ # Writer to override unit assignment
81
+ redefine_method("#{ unit_field_name }=") do |incoming|
82
+ unit_name = measured_class.unit_system.unit_for(incoming).try!(:name)
83
+ write_attribute(unit_field_name, unit_name || incoming)
84
+ end
85
+ end
86
+ end
87
+
88
+ def measured_fields
89
+ @measured_fields ||= {}
90
+ end
91
+
92
+ end
93
+
94
+ module Length
95
+ extend ActiveSupport::Concern
96
+
97
+ module ClassMethods
98
+ def measured_length(*fields)
99
+ measured(Measured::Length, *fields)
100
+ end
101
+ end
102
+ end
103
+
104
+ module Volume
105
+ extend ActiveSupport::Concern
106
+
107
+ module ClassMethods
108
+ def measured_volume(*fields)
109
+ measured(Measured::Volume, *fields)
110
+ end
111
+ end
112
+ end
113
+
114
+ module Weight
115
+ extend ActiveSupport::Concern
116
+
117
+ module ClassMethods
118
+ def measured_weight(*fields)
119
+ measured(Measured::Weight, *fields)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ ::ActiveRecord::Base.include(
126
+ Measured::Rails::ActiveRecord,
127
+ Measured::Rails::ActiveRecord::Length,
128
+ Measured::Rails::ActiveRecord::Volume,
129
+ Measured::Rails::ActiveRecord::Weight,
130
+ )
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations"
4
+
5
+ class MeasuredValidator < ActiveModel::EachValidator
6
+ CHECKS = {
7
+ greater_than: :>,
8
+ greater_than_or_equal_to: :>=,
9
+ equal_to: :==,
10
+ less_than: :<,
11
+ less_than_or_equal_to: :<=,
12
+ }.freeze
13
+
14
+ def validate_each(record, attribute, measurable)
15
+ measured_config = record.class.measured_fields[attribute]
16
+ unit_field_name = measured_config[:unit_field_name] || "#{ attribute }_unit"
17
+ value_field_name = measured_config[:value_field_name] || "#{ attribute }_value"
18
+
19
+ measured_class = measured_config[:class]
20
+
21
+ measurable_unit_name = record.public_send(unit_field_name)
22
+ measurable_value = record.public_send(value_field_name)
23
+
24
+ return unless measurable_unit_name.present? || measurable_value.present?
25
+
26
+ measurable_unit = measured_class.unit_system.unit_for(measurable_unit_name)
27
+ record.errors.add(attribute, message(record, "is not a valid unit")) unless measurable_unit
28
+
29
+ if options[:units] && measurable_unit.present?
30
+ valid_units = Array(options[:units]).map { |unit| measured_class.unit_system.unit_for(unit) }
31
+ record.errors.add(attribute, message(record, "is not a valid unit")) unless valid_units.include?(measurable_unit)
32
+ end
33
+
34
+ if measurable_unit && measurable_value.present?
35
+ options.slice(*CHECKS.keys).each do |option, value|
36
+ comparable_value = value_for(value, record)
37
+ comparable_value = measured_class.new(comparable_value, measurable_unit) unless comparable_value.is_a?(Measured::Measurable)
38
+ unless measurable.public_send(CHECKS[option], comparable_value)
39
+ record.errors.add(attribute, message(record, "#{measurable.to_s} must be #{CHECKS[option]} #{comparable_value}"))
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def message(record, default_message)
48
+ if options[:message].respond_to?(:call)
49
+ options[:message].call(record)
50
+ else
51
+ options[:message] || default_message
52
+ end
53
+ end
54
+
55
+ def value_for(key, record)
56
+ value = case key
57
+ when Proc
58
+ key.call(record)
59
+ when Symbol
60
+ record.send(key)
61
+ else
62
+ key
63
+ end
64
+
65
+ raise ArgumentError, ":#{ value } must be a number or a Measurable object" unless (value.is_a?(Numeric) || value.is_a?(Measured::Measurable))
66
+ value
67
+ end
68
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Measured
4
+ class Rails < ::Rails::Railtie
5
+ class Error < StandardError ; end
6
+
7
+ ActiveSupport.on_load(:active_record) do
8
+ require "measured/rails/active_record"
9
+ require "measured/rails/validations"
10
+ end
11
+ end
12
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  Measured::Weight = Measured.build do
3
- si_unit :g, aliases: [:gram, :grams]
3
+ si_unit :g, aliases: [:gram, :grams, :gm, :gms]
4
4
 
5
5
  unit :t, value: "1000 kg", aliases: [:metric_ton, :metric_tons]
6
6
  unit :slug, value: "14.593903 kg", aliases: [:slugs]
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Measured
3
- VERSION = "2.8.2"
3
+ VERSION = "3.0.0"
4
4
  end
data/lib/measured.rb CHANGED
@@ -4,3 +4,5 @@ require "measured/base"
4
4
  require "measured/units/length"
5
5
  require "measured/units/weight"
6
6
  require "measured/units/volume"
7
+
8
+ require "measured/railtie" if defined?(Rails::Railtie)
@@ -0,0 +1,110 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(::Measured::Rails)
5
+
6
+ module Tapioca
7
+ module Dsl
8
+ module Compilers
9
+ # `Tapioca::Dsl::Compilers::MeasuredRails` refines RBI files for subclasses of
10
+ # [`ActiveRecord::Base`](https://api.rubyonrails.org/classes/ActiveRecord/Base.html)
11
+ # that utilize the [`measured-rails`](https://github.com/shopify/measured-rails) DSL.
12
+ # This compiler is only responsible for defining the methods that would be created
13
+ # for measured fields that are defined in the Active Record model.
14
+ #
15
+ # For example, with the following model class:
16
+ #
17
+ # ~~~rb
18
+ # class Package < ActiveRecord::Base
19
+ # measured Measured::Weight, :minimum_weight
20
+ # measured Measured::Length, :total_length
21
+ # measured Measured::Volume, :total_volume
22
+ # end
23
+ # ~~~
24
+ #
25
+ # this compiler will produce the following methods in the RBI file
26
+ # `package.rbi`:
27
+ #
28
+ # ~~~rbi
29
+ # # package.rbi
30
+ # # typed: true
31
+ #
32
+ # class Package
33
+ # include GeneratedMeasuredRailsMethods
34
+ #
35
+ # module GeneratedMeasuredRailsMethods
36
+ # sig { returns(T.nilable(Measured::Weight)) }
37
+ # def minimum_weight; end
38
+ #
39
+ # sig { params(value: T.nilable(Measured::Weight)).void }
40
+ # def minimum_weight=(value); end
41
+ #
42
+ # sig { returns(T.nilable(Measured::Length)) }
43
+ # def total_length; end
44
+ #
45
+ # sig { params(value: T.nilable(Measured::Length)).void }
46
+ # def total_length=(value); end
47
+ #
48
+ # sig { returns(T.nilable(Measured::Volume)) }
49
+ # def total_volume; end
50
+ #
51
+ # sig { params(value: T.nilable(Measured::Volume)).void }
52
+ # def total_volume=(value); end
53
+ # end
54
+ # end
55
+ # ~~~
56
+ class MeasuredRails < ::Tapioca::Dsl::Compiler
57
+ extend T::Sig
58
+
59
+ ConstantType = type_member { {
60
+ fixed: T.all(
61
+ T.class_of(::ActiveRecord::Base),
62
+ ::Measured::Rails::ActiveRecord::ClassMethods
63
+ )
64
+ } }
65
+
66
+ MeasuredMethodsModuleName = T.let("GeneratedMeasuredRailsMethods", String)
67
+
68
+ sig { override.void }
69
+ def decorate
70
+ return if constant.measured_fields.empty?
71
+
72
+ root.create_path(constant) do |model|
73
+ model.create_module(MeasuredMethodsModuleName) do |mod|
74
+ populate_measured_methods(mod)
75
+ end
76
+
77
+ model.create_include(MeasuredMethodsModuleName)
78
+ end
79
+ end
80
+
81
+ sig { override.returns(T::Enumerable[Module]) }
82
+ def self.gather_constants
83
+ descendants_of(::ActiveRecord::Base)
84
+ end
85
+
86
+ private
87
+
88
+ sig { params(model: RBI::Scope).void }
89
+ def populate_measured_methods(model)
90
+ constant.measured_fields.each do |field, attrs|
91
+ class_name = qualified_name_of(attrs[:class])
92
+
93
+ next unless class_name
94
+
95
+ model.create_method(
96
+ field.to_s,
97
+ return_type: as_nilable_type(class_name)
98
+ )
99
+
100
+ model.create_method(
101
+ "#{field}=",
102
+ parameters: [create_param("value", type: as_nilable_type(class_name))],
103
+ return_type: "void"
104
+ )
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
data/measured.gemspec CHANGED
@@ -13,6 +13,8 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/Shopify/measured"
14
14
  spec.license = "MIT"
15
15
 
16
+ spec.required_ruby_version = ">= 3.0.0"
17
+
16
18
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
17
19
  # delete this section to allow pushing this gem to any host.
18
20
  if spec.respond_to?(:metadata)
@@ -33,4 +35,7 @@ Gem::Specification.new do |spec|
33
35
  spec.add_development_dependency "minitest-reporters"
34
36
  spec.add_development_dependency "mocha", ">= 1.4.0"
35
37
  spec.add_development_dependency "pry"
38
+ spec.add_development_dependency "combustion"
39
+ spec.add_development_dependency "sqlite3", "~> 1.4"
40
+ spec.add_development_dependency "tapioca"
36
41
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+ class Thing < ActiveRecord::Base
3
+
4
+ measured_length :length, :width
5
+
6
+ measured Measured::Length, :height
7
+ measured Measured::Volume, :volume
8
+
9
+ measured_weight :total_weight
10
+
11
+ measured "Measured::Weight", :extra_weight
12
+
13
+ measured_length :length_with_max_on_assignment, {max_on_assignment: 500}
14
+ end