measured 2.8.2 → 3.0.0

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