measured 2.8.2 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +11 -5
- data/.github/workflows/cla.yml +23 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -0
- data/README.md +114 -4
- data/dev.yml +1 -2
- data/gemfiles/{activesupport-6.0.gemfile → rails-6.0.gemfile} +1 -0
- data/gemfiles/{activesupport-6.1.gemfile → rails-6.1.gemfile} +1 -0
- data/gemfiles/rails-7.0.gemfile +6 -0
- data/gemfiles/rails-edge.gemfile +6 -0
- data/lib/measured/measurable.rb +3 -1
- data/lib/measured/rails/active_record.rb +130 -0
- data/lib/measured/rails/validations.rb +68 -0
- data/lib/measured/railtie.rb +12 -0
- data/lib/measured/units/weight.rb +1 -1
- data/lib/measured/version.rb +1 -1
- data/lib/measured.rb +2 -0
- data/lib/tapioca/dsl/compilers/measured_rails.rb +110 -0
- data/measured.gemspec +5 -0
- data/test/internal/app/models/thing.rb +14 -0
- data/test/internal/app/models/thing_with_custom_unit_accessor.rb +18 -0
- data/test/internal/app/models/thing_with_custom_value_accessor.rb +19 -0
- data/test/internal/app/models/validated_thing.rb +45 -0
- data/test/internal/config/database.yml +3 -0
- data/test/internal/config.ru +9 -0
- data/test/internal/db/.gitignore +1 -0
- data/test/internal/db/schema.rb +99 -0
- data/test/internal/log/.gitignore +1 -0
- data/test/measurable_test.rb +4 -0
- data/test/rails/active_record_test.rb +433 -0
- data/test/rails/validation_test.rb +252 -0
- data/test/tapioca/dsl/compilers/measured_rails_test.rb +220 -0
- data/test/test_helper.rb +15 -0
- data/test/units/weight_test.rb +3 -1
- metadata +80 -7
- data/gemfiles/activesupport-5.2.gemfile +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b794b67ca5a9cc00444ec39526340e3f3710dc0395b7205aeb95f44d8ed698fe
|
4
|
+
data.tar.gz: 2696f28b014ce76cded21150a1e1b4ef4772ef92a358ddf4e99b526081125d2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 95c82144964349b7111b6a1d372384dc3721b3c5e79870c8b841b7468d6794f60702c2acb1be714fab6c5a858a26d0bc28c340ef987c61a5d94658ffd64eec25
|
7
|
+
data.tar.gz: 49b6f68fd80cce3886824df3298c2d837ec0f257b0e4bb311231558b0d9a81eb9cb744f3746540eeaa740f6ad1f84bb578441fc6914841ac81ae57005638a1c0
|
data/.github/workflows/ci.yml
CHANGED
@@ -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/
|
19
|
-
- gemfiles/
|
20
|
-
- gemfiles/
|
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
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
|
-
|
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
|
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
|
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
|
-
*
|
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
data/lib/measured/measurable.rb
CHANGED
@@ -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]
|
data/lib/measured/version.rb
CHANGED
data/lib/measured.rb
CHANGED
@@ -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
|