measured 2.8.2 → 3.1.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.

Potentially problematic release.


This version of measured might be problematic. Click here for more details.

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