measured 2.8.2 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +20 -0
- data/.github/workflows/ci.yml +12 -6
- data/.github/workflows/cla.yml +23 -0
- data/.github/workflows/dependabot_auto_merge.yml +93 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +2 -0
- data/README.md +114 -4
- data/cache/weight.json +230 -0
- data/dev.yml +1 -2
- data/gemfiles/rails-7.0.gemfile +6 -0
- data/gemfiles/rails-7.1.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 +3 -2
- 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 +77 -2
- metadata +84 -10
- data/gemfiles/activesupport-5.2.gemfile +0 -5
- data/gemfiles/activesupport-6.0.gemfile +0 -5
- data/gemfiles/activesupport-6.1.gemfile +0 -5
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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class ThingWithCustomUnitAccessor < ActiveRecord::Base
|
3
|
+
measured_length :length, :width, unit_field_name: :size_unit
|
4
|
+
validates :length, measured: true
|
5
|
+
validates :width, measured: true
|
6
|
+
|
7
|
+
measured_volume :volume
|
8
|
+
validates :volume, measured: true
|
9
|
+
|
10
|
+
measured Measured::Length, :height, unit_field_name: :size_unit
|
11
|
+
validates :height, measured: true
|
12
|
+
|
13
|
+
measured_weight :total_weight, unit_field_name: :weight_unit
|
14
|
+
validates :total_weight, measured: true
|
15
|
+
|
16
|
+
measured "Measured::Weight", :extra_weight, unit_field_name: :weight_unit
|
17
|
+
validates :extra_weight, measured: true
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class ThingWithCustomValueAccessor < ActiveRecord::Base
|
3
|
+
measured_length :length, value_field_name: :custom_length
|
4
|
+
validates :length, measured: true
|
5
|
+
measured_length :width, value_field_name: :custom_width
|
6
|
+
validates :width, measured: true
|
7
|
+
|
8
|
+
measured_volume :volume, value_field_name: :custom_volume
|
9
|
+
validates :volume, measured: true
|
10
|
+
|
11
|
+
measured_length :height, value_field_name: :custom_height
|
12
|
+
validates :height, measured: true
|
13
|
+
|
14
|
+
measured_weight :total_weight, value_field_name: :custom_weight
|
15
|
+
validates :total_weight, measured: true
|
16
|
+
|
17
|
+
measured_weight :extra_weight, value_field_name: :custom_extra_weight
|
18
|
+
validates :extra_weight, measured: true
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class ValidatedThing < ActiveRecord::Base
|
3
|
+
measured_length :length
|
4
|
+
validates :length, measured: true
|
5
|
+
|
6
|
+
measured_length :length_true
|
7
|
+
validates :length_true, measured: true
|
8
|
+
|
9
|
+
measured_length :length_message
|
10
|
+
validates :length_message, measured: {message: "has a custom failure message"}
|
11
|
+
|
12
|
+
measured_length :length_message_from_block
|
13
|
+
validates :length_message_from_block, measured: { message: Proc.new { |record| "#{record.length_message_from_block_unit} is not a valid unit" } }
|
14
|
+
|
15
|
+
measured_length :length_units
|
16
|
+
validates :length_units, measured: {units: [:meter, "cm"]}
|
17
|
+
|
18
|
+
measured_length :length_units_singular
|
19
|
+
validates :length_units_singular, measured: {units: :ft, message: "custom message too"}
|
20
|
+
|
21
|
+
measured_length :length_presence
|
22
|
+
validates :length_presence, measured: true, presence: true
|
23
|
+
|
24
|
+
measured_length :length_numericality_inclusive
|
25
|
+
validates :length_numericality_inclusive, measured: {greater_than_or_equal_to: :low_bound, less_than_or_equal_to: :high_bound }
|
26
|
+
|
27
|
+
measured_length :length_numericality_exclusive
|
28
|
+
validates :length_numericality_exclusive, measured: {greater_than: Measured::Length.new(3, :m), less_than: Measured::Length.new(500, :cm), message: "is super not ok"}
|
29
|
+
|
30
|
+
measured_length :length_numericality_equality
|
31
|
+
validates :length_numericality_equality, measured: {equal_to: Proc.new { Measured::Length.new(100, :cm) }, message: "must be exactly 100cm"}
|
32
|
+
|
33
|
+
measured_length :length_invalid_comparison
|
34
|
+
validates :length_invalid_comparison, measured: {equal_to: "not_a_measured_subclass"}
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def low_bound
|
39
|
+
Measured::Length.new(10, :in)
|
40
|
+
end
|
41
|
+
|
42
|
+
def high_bound
|
43
|
+
Measured::Length.new(20, :in)
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
measured.sqlite*
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# This file is auto-generated from the current state of the database. Instead
|
2
|
+
# of editing this file, please use the migrations feature of Active Record to
|
3
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
4
|
+
#
|
5
|
+
# Note that this schema.rb definition is the authoritative source for your
|
6
|
+
# database schema. If you need to create the application database on another
|
7
|
+
# system, you should be using db:schema:load, not running all the migrations
|
8
|
+
# from scratch. The latter is a flawed and unsustainable approach (the more migrations
|
9
|
+
# you'll amass, the slower it'll run and the greater likelihood for issues).
|
10
|
+
#
|
11
|
+
# It's strongly recommended that you check this file into your version control system.
|
12
|
+
|
13
|
+
ActiveRecord::Schema.define do
|
14
|
+
create_table "thing_with_custom_unit_accessors", force: :cascade do |t|
|
15
|
+
t.decimal "length_value", precision: 10, scale: 2
|
16
|
+
t.decimal "width_value", precision: 10, scale: 2
|
17
|
+
t.decimal "height_value", precision: 10, scale: 2
|
18
|
+
t.decimal "volume_value", precision: 10, scale: 2
|
19
|
+
t.string "volume_unit", limit: 12
|
20
|
+
t.string "size_unit", limit: 12
|
21
|
+
t.decimal "total_weight_value", precision: 10, scale: 2, default: "10.0"
|
22
|
+
t.decimal "extra_weight_value", precision: 10, scale: 2
|
23
|
+
t.string "weight_unit", limit: 12
|
24
|
+
t.datetime "created_at", null: false
|
25
|
+
t.datetime "updated_at", null: false
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table "things", force: :cascade do |t|
|
29
|
+
t.decimal "length_value", precision: 10, scale: 2
|
30
|
+
t.string "length_unit", limit: 12
|
31
|
+
t.decimal "width_value", precision: 10, scale: 2
|
32
|
+
t.string "width_unit", limit: 12
|
33
|
+
t.decimal "height_value", precision: 10, scale: 2
|
34
|
+
t.string "height_unit", limit: 12
|
35
|
+
t.decimal "volume_value", precision: 10, scale: 2
|
36
|
+
t.string "volume_unit", limit: 12
|
37
|
+
t.decimal "total_weight_value", precision: 10, scale: 2, default: "10.0"
|
38
|
+
t.string "total_weight_unit", limit: 12, default: "g"
|
39
|
+
t.decimal "extra_weight_value", precision: 10, scale: 2
|
40
|
+
t.string "extra_weight_unit", limit: 12
|
41
|
+
t.decimal "length_with_max_on_assignment_value", precision: 10, scale: 2
|
42
|
+
t.string "length_with_max_on_assignment_unit", limit: 12
|
43
|
+
t.datetime "created_at", null: false
|
44
|
+
t.datetime "updated_at", null: false
|
45
|
+
end
|
46
|
+
|
47
|
+
create_table "validated_things", force: :cascade do |t|
|
48
|
+
t.decimal "length_value", precision: 10, scale: 2
|
49
|
+
t.string "length_unit", limit: 12
|
50
|
+
t.decimal "length_true_value", precision: 10, scale: 2
|
51
|
+
t.string "length_true_unit", limit: 12
|
52
|
+
t.decimal "length_message_value", precision: 10, scale: 2
|
53
|
+
t.string "length_message_unit", limit: 12
|
54
|
+
t.decimal "length_message_from_block_value", precision: 10, scale: 2
|
55
|
+
t.string "length_message_from_block_unit", limit: 12
|
56
|
+
t.decimal "length_units_value", precision: 10, scale: 2
|
57
|
+
t.string "length_units_unit", limit: 12
|
58
|
+
t.decimal "length_units_singular_value", precision: 10, scale: 2
|
59
|
+
t.string "length_units_singular_unit", limit: 12
|
60
|
+
t.decimal "length_presence_value", precision: 10, scale: 2
|
61
|
+
t.string "length_presence_unit", limit: 12
|
62
|
+
t.decimal "length_invalid_value", precision: 10, scale: 2
|
63
|
+
t.string "length_invalid_unit", limit: 12
|
64
|
+
t.datetime "created_at", null: false
|
65
|
+
t.datetime "updated_at", null: false
|
66
|
+
t.decimal "length_numericality_inclusive_value", precision: 10, scale: 2
|
67
|
+
t.string "length_numericality_inclusive_unit", limit: 12
|
68
|
+
t.decimal "length_numericality_exclusive_value", precision: 10, scale: 2
|
69
|
+
t.string "length_numericality_exclusive_unit", limit: 12
|
70
|
+
t.decimal "length_numericality_equality_value", precision: 10, scale: 2
|
71
|
+
t.string "length_numericality_equality_unit", limit: 12
|
72
|
+
t.decimal "length_invalid_comparison_value", precision: 10, scale: 2
|
73
|
+
t.string "length_invalid_comparison_unit", limit: 12
|
74
|
+
t.decimal "length_non_zero_scalar_value", precision: 10, scale: 2
|
75
|
+
t.string "length_non_zero_scalar_unit", limit: 12
|
76
|
+
t.decimal "length_zero_scalar_value", precision: 10, scale: 2
|
77
|
+
t.string "length_zero_scalar_unit", limit: 12
|
78
|
+
t.decimal "length_numericality_less_than_than_scalar_value", precision: 10, scale: 2
|
79
|
+
t.string "length_numericality_less_than_than_scalar_unit", limit: 12
|
80
|
+
end
|
81
|
+
|
82
|
+
create_table "thing_with_custom_value_accessors", force: :cascade do |t|
|
83
|
+
t.decimal "custom_length", precision: 10, scale: 2
|
84
|
+
t.string "length_unit", limit: 12
|
85
|
+
t.decimal "custom_width", precision: 10, scale: 2
|
86
|
+
t.string "width_unit", limit: 12
|
87
|
+
t.decimal "custom_height", precision: 10, scale: 2
|
88
|
+
t.string "height_unit", limit: 12
|
89
|
+
t.decimal "custom_volume", precision: 10, scale: 2
|
90
|
+
t.string "volume_unit", limit: 12
|
91
|
+
t.decimal "custom_weight", precision: 10, scale: 2, default: "10.0"
|
92
|
+
t.string "total_weight_unit", limit: 12
|
93
|
+
t.decimal "custom_extra_weight", precision: 10, scale: 2
|
94
|
+
t.string "extra_weight_unit", limit: 12
|
95
|
+
t.datetime "created_at", null: false
|
96
|
+
t.datetime "updated_at", null: false
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
*.log
|
data/test/measurable_test.rb
CHANGED
@@ -41,6 +41,10 @@ class Measured::MeasurableTest < ActiveSupport::TestCase
|
|
41
41
|
assert_equal BigDecimal("9.1234572342342"), Magic.new("9.1234572342342", :fire).value
|
42
42
|
end
|
43
43
|
|
44
|
+
test "#initialize converts strings to Rational if they follow Rational pattern" do
|
45
|
+
assert_equal Rational(1, 3), Magic.new("1/3", :fire).value
|
46
|
+
end
|
47
|
+
|
44
48
|
test "#initialize converts to the base unit" do
|
45
49
|
assert_equal @fireball, Magic.new(1, :fire).unit
|
46
50
|
end
|