measured 2.8.2 → 3.1.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 +4 -4
  2. data/.github/dependabot.yml +20 -0
  3. data/.github/workflows/ci.yml +12 -6
  4. data/.github/workflows/cla.yml +23 -0
  5. data/.github/workflows/dependabot_auto_merge.yml +93 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +13 -0
  8. data/Gemfile +2 -0
  9. data/README.md +114 -4
  10. data/cache/weight.json +230 -0
  11. data/dev.yml +1 -2
  12. data/gemfiles/rails-7.0.gemfile +6 -0
  13. data/gemfiles/rails-7.1.gemfile +6 -0
  14. data/gemfiles/rails-edge.gemfile +6 -0
  15. data/lib/measured/measurable.rb +3 -1
  16. data/lib/measured/rails/active_record.rb +130 -0
  17. data/lib/measured/rails/validations.rb +68 -0
  18. data/lib/measured/railtie.rb +12 -0
  19. data/lib/measured/units/weight.rb +3 -2
  20. data/lib/measured/version.rb +1 -1
  21. data/lib/measured.rb +2 -0
  22. data/lib/tapioca/dsl/compilers/measured_rails.rb +110 -0
  23. data/measured.gemspec +5 -0
  24. data/test/internal/app/models/thing.rb +14 -0
  25. data/test/internal/app/models/thing_with_custom_unit_accessor.rb +18 -0
  26. data/test/internal/app/models/thing_with_custom_value_accessor.rb +19 -0
  27. data/test/internal/app/models/validated_thing.rb +45 -0
  28. data/test/internal/config/database.yml +3 -0
  29. data/test/internal/config.ru +9 -0
  30. data/test/internal/db/.gitignore +1 -0
  31. data/test/internal/db/schema.rb +99 -0
  32. data/test/internal/log/.gitignore +1 -0
  33. data/test/measurable_test.rb +4 -0
  34. data/test/rails/active_record_test.rb +433 -0
  35. data/test/rails/validation_test.rb +252 -0
  36. data/test/tapioca/dsl/compilers/measured_rails_test.rb +220 -0
  37. data/test/test_helper.rb +15 -0
  38. data/test/units/weight_test.rb +77 -2
  39. metadata +84 -10
  40. data/gemfiles/activesupport-5.2.gemfile +0 -5
  41. data/gemfiles/activesupport-6.0.gemfile +0 -5
  42. data/gemfiles/activesupport-6.1.gemfile +0 -5
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
@@ -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,3 @@
1
+ test:
2
+ adapter: sqlite3
3
+ database: ":memory:"
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
@@ -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
@@ -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