iknow_view_models 3.1.8 → 3.2.4
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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +6 -6
- data/.rubocop.yml +18 -0
- data/Appraisals +6 -6
- data/Gemfile +6 -2
- data/Rakefile +5 -5
- data/gemfiles/rails_5_2.gemfile +5 -5
- data/gemfiles/rails_6_0.gemfile +9 -0
- data/iknow_view_models.gemspec +40 -38
- data/lib/iknow_view_models.rb +9 -7
- data/lib/iknow_view_models/version.rb +1 -1
- data/lib/view_model.rb +31 -17
- data/lib/view_model/access_control.rb +5 -2
- data/lib/view_model/access_control/composed.rb +10 -9
- data/lib/view_model/access_control/open.rb +2 -0
- data/lib/view_model/access_control/read_only.rb +2 -0
- data/lib/view_model/access_control/tree.rb +11 -6
- data/lib/view_model/access_control_error.rb +4 -1
- data/lib/view_model/active_record.rb +17 -15
- data/lib/view_model/active_record/association_data.rb +2 -1
- data/lib/view_model/active_record/association_manipulation.rb +6 -4
- data/lib/view_model/active_record/cache.rb +114 -34
- data/lib/view_model/active_record/cache/cacheable_view.rb +2 -2
- data/lib/view_model/active_record/collection_nested_controller.rb +3 -3
- data/lib/view_model/active_record/controller.rb +68 -1
- data/lib/view_model/active_record/controller_base.rb +4 -1
- data/lib/view_model/active_record/nested_controller_base.rb +1 -0
- data/lib/view_model/active_record/update_context.rb +8 -6
- data/lib/view_model/active_record/update_data.rb +32 -30
- data/lib/view_model/active_record/update_operation.rb +17 -13
- data/lib/view_model/active_record/visitor.rb +0 -1
- data/lib/view_model/after_transaction_runner.rb +0 -1
- data/lib/view_model/callbacks.rb +3 -1
- data/lib/view_model/controller.rb +13 -3
- data/lib/view_model/deserialization_error.rb +15 -12
- data/lib/view_model/error.rb +12 -10
- data/lib/view_model/error_view.rb +3 -1
- data/lib/view_model/migratable_view.rb +78 -0
- data/lib/view_model/migration.rb +48 -0
- data/lib/view_model/migration/no_path_error.rb +26 -0
- data/lib/view_model/migration/one_way_error.rb +24 -0
- data/lib/view_model/migration/unspecified_version_error.rb +24 -0
- data/lib/view_model/migrator.rb +108 -0
- data/lib/view_model/record.rb +15 -14
- data/lib/view_model/reference.rb +3 -1
- data/lib/view_model/references.rb +8 -5
- data/lib/view_model/registry.rb +14 -2
- data/lib/view_model/schemas.rb +9 -4
- data/lib/view_model/serialization_error.rb +4 -1
- data/lib/view_model/serialize_context.rb +4 -4
- data/lib/view_model/test_helpers.rb +8 -3
- data/lib/view_model/test_helpers/arvm_builder.rb +21 -15
- data/lib/view_model/traversal_context.rb +2 -1
- data/nix/dependencies.nix +5 -0
- data/nix/gem/generate.rb +2 -1
- data/shell.nix +8 -3
- data/test/.rubocop.yml +14 -0
- data/test/helpers/arvm_test_models.rb +12 -9
- data/test/helpers/arvm_test_utilities.rb +5 -3
- data/test/helpers/controller_test_helpers.rb +55 -32
- data/test/helpers/match_enumerator.rb +1 -0
- data/test/helpers/query_logging.rb +2 -1
- data/test/helpers/test_access_control.rb +5 -3
- data/test/helpers/viewmodel_spec_helpers.rb +88 -22
- data/test/unit/view_model/access_control_test.rb +144 -144
- data/test/unit/view_model/active_record/alias_test.rb +15 -13
- data/test/unit/view_model/active_record/belongs_to_test.rb +40 -39
- data/test/unit/view_model/active_record/cache_test.rb +68 -31
- data/test/unit/view_model/active_record/cloner_test.rb +67 -63
- data/test/unit/view_model/active_record/controller_test.rb +113 -65
- data/test/unit/view_model/active_record/counter_test.rb +10 -9
- data/test/unit/view_model/active_record/customization_test.rb +59 -58
- data/test/unit/view_model/active_record/has_many_test.rb +112 -111
- data/test/unit/view_model/active_record/has_many_through_poly_test.rb +15 -14
- data/test/unit/view_model/active_record/has_many_through_test.rb +33 -38
- data/test/unit/view_model/active_record/has_one_test.rb +37 -36
- data/test/unit/view_model/active_record/migration_test.rb +161 -0
- data/test/unit/view_model/active_record/namespacing_test.rb +19 -17
- data/test/unit/view_model/active_record/poly_test.rb +44 -45
- data/test/unit/view_model/active_record/shared_test.rb +30 -28
- data/test/unit/view_model/active_record/version_test.rb +9 -7
- data/test/unit/view_model/active_record_test.rb +72 -72
- data/test/unit/view_model/callbacks_test.rb +19 -15
- data/test/unit/view_model/controller_test.rb +4 -2
- data/test/unit/view_model/record_test.rb +158 -145
- data/test/unit/view_model/registry_test.rb +38 -0
- data/test/unit/view_model/traversal_context_test.rb +4 -5
- data/test/unit/view_model_test.rb +18 -16
- metadata +38 -12
- data/.travis.yml +0 -31
- data/appveyor.yml +0 -22
- data/gemfiles/rails_6_0_beta.gemfile +0 -9
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class ViewModel::Migration::UnspecifiedVersionError < ViewModel::AbstractError
         | 
| 4 | 
            +
              attr_reader :vm_name, :version
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              status 400
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              def initialize(vm_name, version)
         | 
| 9 | 
            +
                @vm_name = vm_name
         | 
| 10 | 
            +
                @version = version
         | 
| 11 | 
            +
                super()
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def detail
         | 
| 15 | 
            +
                "Provided view for #{vm_name} at version #{version} does not match request"
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              def meta
         | 
| 19 | 
            +
                {
         | 
| 20 | 
            +
                  viewmodel: vm_name,
         | 
| 21 | 
            +
                  version: version,
         | 
| 22 | 
            +
                }
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,108 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class ViewModel
         | 
| 4 | 
            +
              class Migrator
         | 
| 5 | 
            +
                class << self
         | 
| 6 | 
            +
                  def migrated_deep_schema_version(viewmodel_class, required_versions, include_referenced: true)
         | 
| 7 | 
            +
                    deep_schema_version = viewmodel_class.deep_schema_version(include_referenced: include_referenced)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    if required_versions.present?
         | 
| 10 | 
            +
                      deep_schema_version = deep_schema_version.dup
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      required_versions.each do |required_vm_class, required_version|
         | 
| 13 | 
            +
                        name = required_vm_class.view_name
         | 
| 14 | 
            +
                        if deep_schema_version.has_key?(name)
         | 
| 15 | 
            +
                          deep_schema_version[name] = required_version
         | 
| 16 | 
            +
                        end
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    deep_schema_version
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def initialize(required_versions)
         | 
| 25 | 
            +
                  @paths = required_versions.each_with_object({}) do |(viewmodel_class, required_version), h|
         | 
| 26 | 
            +
                    if required_version != viewmodel_class.schema_version
         | 
| 27 | 
            +
                      path = viewmodel_class.migration_path(from: required_version, to: viewmodel_class.schema_version)
         | 
| 28 | 
            +
                      h[viewmodel_class.view_name] = path
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  @versions = required_versions.each_with_object({}) do |(viewmodel_class, required_version), h|
         | 
| 33 | 
            +
                    h[viewmodel_class.view_name] = [required_version, viewmodel_class.schema_version]
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def migrate!(node, references:)
         | 
| 38 | 
            +
                  case node
         | 
| 39 | 
            +
                  when Hash
         | 
| 40 | 
            +
                    if (type = node[ViewModel::TYPE_ATTRIBUTE])
         | 
| 41 | 
            +
                      version = node[ViewModel::VERSION_ATTRIBUTE]
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                      if migrate_viewmodel!(type, version, node, references)
         | 
| 44 | 
            +
                        node[ViewModel::MIGRATED_ATTRIBUTE] = true
         | 
| 45 | 
            +
                      end
         | 
| 46 | 
            +
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    node.each_value do |child|
         | 
| 49 | 
            +
                      migrate!(child, references: references)
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  when Array
         | 
| 52 | 
            +
                    node.each { |child| migrate!(child, references: references) }
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                private
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def migrate_viewmodel!(_view_name, _version, _view_hash, _references)
         | 
| 59 | 
            +
                  raise RuntimeError.new('abstract method')
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              class UpMigrator < Migrator
         | 
| 64 | 
            +
                private
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                def migrate_viewmodel!(view_name, source_version, view_hash, references)
         | 
| 67 | 
            +
                  path = @paths[view_name]
         | 
| 68 | 
            +
                  return false unless path
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  # We assume that an unspecified source version is the same as the required
         | 
| 71 | 
            +
                  # version.
         | 
| 72 | 
            +
                  required_version, current_version = @versions[view_name]
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  unless source_version.nil? || source_version == required_version
         | 
| 75 | 
            +
                    raise ViewModel::Migration::UnspecifiedVersionError.new(view_name, source_version)
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  path.each do |migration|
         | 
| 79 | 
            +
                    migration.up(view_hash, references)
         | 
| 80 | 
            +
                  end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                  view_hash[ViewModel::VERSION_ATTRIBUTE] = current_version
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  true
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              # down migrations find a reverse path from the current schema version to the
         | 
| 89 | 
            +
              # specific version requested by the client.
         | 
| 90 | 
            +
              class DownMigrator < Migrator
         | 
| 91 | 
            +
                private
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def migrate_viewmodel!(view_name, _, view_hash, references)
         | 
| 94 | 
            +
                  path = @paths[view_name]
         | 
| 95 | 
            +
                  return false unless path
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  required_version, _current_version = @versions[view_name]
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                  path.reverse_each do |migration|
         | 
| 100 | 
            +
                    migration.down(view_hash, references)
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                  view_hash[ViewModel::VERSION_ATTRIBUTE] = required_version
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                  true
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
              end
         | 
| 108 | 
            +
            end
         | 
    
        data/lib/view_model/record.rb
    CHANGED
    
    | @@ -10,6 +10,9 @@ class ViewModel::Record < ViewModel | |
| 10 10 | 
             
              attr_accessor :model
         | 
| 11 11 |  | 
| 12 12 | 
             
              require 'view_model/record/attribute_data'
         | 
| 13 | 
            +
              require 'view_model/migratable_view'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              include ViewModel::MigratableView
         | 
| 13 16 |  | 
| 14 17 | 
             
              class << self
         | 
| 15 18 | 
             
                attr_reader :_members
         | 
| @@ -113,7 +116,7 @@ class ViewModel::Record < ViewModel | |
| 113 116 | 
             
                  end
         | 
| 114 117 | 
             
                end
         | 
| 115 118 |  | 
| 116 | 
            -
                def resolve_viewmodel( | 
| 119 | 
            +
                def resolve_viewmodel(_metadata, _view_hash, deserialize_context:)
         | 
| 117 120 | 
             
                  self.for_new_model
         | 
| 118 121 | 
             
                end
         | 
| 119 122 |  | 
| @@ -178,6 +181,8 @@ class ViewModel::Record < ViewModel | |
| 178 181 | 
             
                @changed_attributes            = []
         | 
| 179 182 | 
             
                @changed_nested_children       = false
         | 
| 180 183 | 
             
                @changed_referenced_children   = false
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                super()
         | 
| 181 186 | 
             
              end
         | 
| 182 187 |  | 
| 183 188 | 
             
              # VM::Record identity matches the identity of its model. If the model has a
         | 
| @@ -207,7 +212,7 @@ class ViewModel::Record < ViewModel | |
| 207 212 | 
             
              end
         | 
| 208 213 |  | 
| 209 214 | 
             
              def serialize_view(json, serialize_context: self.class.new_serialize_context)
         | 
| 210 | 
            -
                json.set!(ViewModel::ID_ATTRIBUTE,  | 
| 215 | 
            +
                json.set!(ViewModel::ID_ATTRIBUTE, self.id) if stable_id?
         | 
| 211 216 | 
             
                json.set!(ViewModel::TYPE_ATTRIBUTE, self.view_name)
         | 
| 212 217 | 
             
                json.set!(ViewModel::VERSION_ATTRIBUTE, self.class.schema_version)
         | 
| 213 218 |  | 
| @@ -306,12 +311,10 @@ class ViewModel::Record < ViewModel | |
| 306 311 | 
             
                  # viewmodel), it's only desired for converting the value to and from wire
         | 
| 307 312 | 
             
                  # format, so conversion is deferred to serialization time.
         | 
| 308 313 | 
             
                  value = attr_data.map_value(value) do |v|
         | 
| 309 | 
            -
                     | 
| 310 | 
            -
             | 
| 311 | 
            -
                     | 
| 312 | 
            -
             | 
| 313 | 
            -
                              "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
         | 
| 314 | 
            -
                    end
         | 
| 314 | 
            +
                    attr_data.attribute_serializer.dump(v, json: true)
         | 
| 315 | 
            +
                  rescue IknowParams::Serializer::DumpError => ex
         | 
| 316 | 
            +
                    raise ViewModel::SerializationError.new(
         | 
| 317 | 
            +
                            "Could not serialize invalid value '#{vm_attr_name}': #{ex.message}")
         | 
| 315 318 | 
             
                  end
         | 
| 316 319 | 
             
                end
         | 
| 317 320 |  | 
| @@ -339,12 +342,10 @@ class ViewModel::Record < ViewModel | |
| 339 342 | 
             
                    end
         | 
| 340 343 | 
             
                  when attr_data.using_serializer?
         | 
| 341 344 | 
             
                    attr_data.map_value(serialized_value) do |sv|
         | 
| 342 | 
            -
                       | 
| 343 | 
            -
             | 
| 344 | 
            -
                       | 
| 345 | 
            -
             | 
| 346 | 
            -
                        raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
         | 
| 347 | 
            -
                      end
         | 
| 345 | 
            +
                      attr_data.attribute_serializer.load(sv)
         | 
| 346 | 
            +
                    rescue IknowParams::Serializer::LoadError => ex
         | 
| 347 | 
            +
                      reason = "could not be deserialized because #{ex.message}"
         | 
| 348 | 
            +
                      raise ViewModel::DeserializationError::Validation.new(vm_attr_name, reason, {}, blame_reference)
         | 
| 348 349 | 
             
                    end
         | 
| 349 350 | 
             
                  else
         | 
| 350 351 | 
             
                    serialized_value
         | 
    
        data/lib/view_model/reference.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            class ViewModel
         | 
| 2 4 | 
             
              # Key to identify a viewmodel with some kind of inherent ID (e.g. an ViewModel::ActiveRecord)
         | 
| 3 5 | 
             
              class Reference
         | 
| @@ -22,7 +24,7 @@ class ViewModel | |
| 22 24 | 
             
                    other.model_id        == model_id
         | 
| 23 25 | 
             
                end
         | 
| 24 26 |  | 
| 25 | 
            -
                alias  | 
| 27 | 
            +
                alias eql? ==
         | 
| 26 28 |  | 
| 27 29 | 
             
                def hash
         | 
| 28 30 | 
             
                  [viewmodel_class, model_id].hash
         | 
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            class ViewModel
         | 
| 2 4 | 
             
              # A bucket for configuration, used for serializing and deserializing.
         | 
| 3 5 | 
             
              class References
         | 
| @@ -17,14 +19,15 @@ class ViewModel | |
| 17 19 | 
             
                # under which the data is stored. If the data is not present, will compute
         | 
| 18 20 | 
             
                # it by calling the given block.
         | 
| 19 21 | 
             
                def add_reference(value)
         | 
| 20 | 
            -
                   | 
| 21 | 
            -
             | 
| 22 | 
            -
                   | 
| 22 | 
            +
                  ref = @ref_by_value[value]
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  unless ref.present?
         | 
| 23 25 | 
             
                    ref = new_ref!(value)
         | 
| 24 26 | 
             
                    @ref_by_value[value] = ref
         | 
| 25 27 | 
             
                    @value_by_ref[ref] = value
         | 
| 26 | 
            -
                    ref
         | 
| 27 28 | 
             
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  ref
         | 
| 28 31 | 
             
                end
         | 
| 29 32 |  | 
| 30 33 | 
             
                def clear!
         | 
| @@ -41,7 +44,7 @@ class ViewModel | |
| 41 44 | 
             
                    hash = Digest::SHA256.base64digest("#{vm_ref.viewmodel_class.name}.#{vm_ref.model_id}")
         | 
| 42 45 | 
             
                    "ref:h:#{hash}"
         | 
| 43 46 | 
             
                  else
         | 
| 44 | 
            -
                    'ref:i:% | 
| 47 | 
            +
                    format('ref:i:%06<count>d', count: (@last_ref += 1))
         | 
| 45 48 | 
             
                  end
         | 
| 46 49 | 
             
                end
         | 
| 47 50 | 
             
              end
         | 
    
        data/lib/view_model/registry.rb
    CHANGED
    
    | @@ -6,13 +6,14 @@ class ViewModel::Registry | |
| 6 6 | 
             
              DEFERRED_NAME = Object.new
         | 
| 7 7 |  | 
| 8 8 | 
             
              class << self
         | 
| 9 | 
            -
                delegate :for_view_name, :register, :default_view_name, :infer_model_class_name, | 
| 9 | 
            +
                delegate :for_view_name, :register, :default_view_name, :infer_model_class_name,
         | 
| 10 | 
            +
                         :clear_removed_classes!, :all, :roots,
         | 
| 10 11 | 
             
                         to: :instance
         | 
| 11 12 | 
             
              end
         | 
| 12 13 |  | 
| 13 14 | 
             
              def initialize
         | 
| 14 15 | 
             
                @lock = Monitor.new
         | 
| 15 | 
            -
                @viewmodel_classes_by_name | 
| 16 | 
            +
                @viewmodel_classes_by_name = {}
         | 
| 16 17 | 
             
                @deferred_viewmodel_classes = []
         | 
| 17 18 | 
             
              end
         | 
| 18 19 |  | 
| @@ -33,6 +34,17 @@ class ViewModel::Registry | |
| 33 34 | 
             
                end
         | 
| 34 35 | 
             
              end
         | 
| 35 36 |  | 
| 37 | 
            +
              def all
         | 
| 38 | 
            +
                @lock.synchronize do
         | 
| 39 | 
            +
                  resolve_deferred_classes
         | 
| 40 | 
            +
                  @viewmodel_classes_by_name.values
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              def roots
         | 
| 45 | 
            +
                all.select { |c| c.root? }
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 36 48 | 
             
              def register(viewmodel, as: DEFERRED_NAME)
         | 
| 37 49 | 
             
                @lock.synchronize do
         | 
| 38 50 | 
             
                  @deferred_viewmodel_classes << [viewmodel, as]
         | 
    
        data/lib/view_model/schemas.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'json'
         | 
| 2 4 | 
             
            require 'json_schema'
         | 
| 3 5 |  | 
| @@ -9,7 +11,8 @@ class ViewModel::Schemas | |
| 9 11 |  | 
| 10 12 | 
             
              ID_SCHEMA =
         | 
| 11 13 | 
             
                { 'oneOf' => [{ 'type' => 'integer' },
         | 
| 12 | 
            -
                              { 'type' => 'string', 'format' => 'uuid' }] }
         | 
| 14 | 
            +
                              { 'type' => 'string', 'format' => 'uuid' },] }.freeze
         | 
| 15 | 
            +
             | 
| 13 16 | 
             
              ID = JsonSchema.parse!(ID_SCHEMA)
         | 
| 14 17 |  | 
| 15 18 | 
             
              VIEWMODEL_UPDATE_SCHEMA =
         | 
| @@ -20,8 +23,9 @@ class ViewModel::Schemas | |
| 20 23 | 
             
                                     ViewModel::ID_ATTRIBUTE      => ID_SCHEMA,
         | 
| 21 24 | 
             
                                     ViewModel::NEW_ATTRIBUTE     => { 'type' => 'boolean' },
         | 
| 22 25 | 
             
                                     ViewModel::VERSION_ATTRIBUTE => { 'type' => 'integer' } },
         | 
| 23 | 
            -
                  'required'    => [ViewModel::TYPE_ATTRIBUTE]
         | 
| 24 | 
            -
                }
         | 
| 26 | 
            +
                  'required'    => [ViewModel::TYPE_ATTRIBUTE],
         | 
| 27 | 
            +
                }.freeze
         | 
| 28 | 
            +
             | 
| 25 29 | 
             
              VIEWMODEL_UPDATE = JsonSchema.parse!(VIEWMODEL_UPDATE_SCHEMA)
         | 
| 26 30 |  | 
| 27 31 | 
             
              VIEWMODEL_REFERENCE_SCHEMA =
         | 
| @@ -31,7 +35,8 @@ class ViewModel::Schemas | |
| 31 35 | 
             
                  'properties'           => { ViewModel::REFERENCE_ATTRIBUTE => { 'type' => 'string' } },
         | 
| 32 36 | 
             
                  'additionalProperties' => false,
         | 
| 33 37 | 
             
                  'required'             => [ViewModel::REFERENCE_ATTRIBUTE],
         | 
| 34 | 
            -
                }
         | 
| 38 | 
            +
                }.freeze
         | 
| 39 | 
            +
             | 
| 35 40 | 
             
              VIEWMODEL_REFERENCE = JsonSchema.parse!(VIEWMODEL_REFERENCE_SCHEMA)
         | 
| 36 41 |  | 
| 37 42 | 
             
              def self.verify_schema!(schema, value)
         | 
| @@ -38,10 +38,10 @@ class ViewModel::SerializeContext < ViewModel::TraversalContext | |
| 38 38 |  | 
| 39 39 | 
             
                while references.present?
         | 
| 40 40 | 
             
                  extract_referenced_views!.each do |ref, value|
         | 
| 41 | 
            -
                     | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
                       | 
| 41 | 
            +
                    next if serialized_refs.has_key?(ref)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    serialized_refs[ref] = Jbuilder.new do |j|
         | 
| 44 | 
            +
                      ViewModel.serialize(value, j, serialize_context: reference_context)
         | 
| 45 45 | 
             
                    end
         | 
| 46 46 | 
             
                  end
         | 
| 47 47 | 
             
                end
         | 
| @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            ##
         | 
| 2 4 | 
             
            # Helpers useful for writing tests for viewmodel implementations
         | 
| 3 5 | 
             
            module ViewModel::TestHelpers
         | 
| @@ -51,11 +53,13 @@ module ViewModel::TestHelpers | |
| 51 53 |  | 
| 52 54 | 
             
              def assert_consistent_record(viewmodel, been_there: Set.new)
         | 
| 53 55 | 
             
                return if been_there.include?(viewmodel.model)
         | 
| 56 | 
            +
             | 
| 54 57 | 
             
                been_there << viewmodel.model
         | 
| 55 58 |  | 
| 56 | 
            -
                 | 
| 59 | 
            +
                case viewmodel
         | 
| 60 | 
            +
                when ViewModel::ActiveRecord
         | 
| 57 61 | 
             
                  assert_model_represents_database(viewmodel.model, been_there: been_there)
         | 
| 58 | 
            -
                 | 
| 62 | 
            +
                when ViewModel::Record
         | 
| 59 63 | 
             
                  viewmodel.class._members.each do |name, attribute_data|
         | 
| 60 64 | 
             
                    if attribute_data.attribute_viewmodel
         | 
| 61 65 | 
             
                      assert_consistent_record(viewmodel.send(name), been_there: been_there)
         | 
| @@ -66,6 +70,7 @@ module ViewModel::TestHelpers | |
| 66 70 |  | 
| 67 71 | 
             
              def assert_model_represents_database(model, been_there: Set.new)
         | 
| 68 72 | 
             
                return if been_there.include?(model)
         | 
| 73 | 
            +
             | 
| 69 74 | 
             
                been_there << model
         | 
| 70 75 |  | 
| 71 76 | 
             
                refute(model.new_record?, 'model represents database entity')
         | 
| @@ -83,7 +88,7 @@ module ViewModel::TestHelpers | |
| 83 88 | 
             
                  next unless association.loaded?
         | 
| 84 89 |  | 
| 85 90 | 
             
                  case
         | 
| 86 | 
            -
                  when association.target | 
| 91 | 
            +
                  when association.target.nil?
         | 
| 87 92 | 
             
                    assert_nil(database_model.association(reflection.name).target,
         | 
| 88 93 | 
             
                               'in memory nil association matches database')
         | 
| 89 94 | 
             
                  when reflection.collection?
         | 
| @@ -1,10 +1,13 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            class ViewModel::TestHelpers::ARVMBuilder
         | 
| 2 4 | 
             
              attr_reader :name, :model, :viewmodel, :namespace
         | 
| 3 5 |  | 
| 4 6 | 
             
              # Building an ARVM requires three blocks, to define schema, model and
         | 
| 5 7 | 
             
              # viewmodel. Support providing these either in an spec argument or as a
         | 
| 6 8 | 
             
              # dsl-style builder.
         | 
| 7 | 
            -
              Spec = Struct.new(:schema, :model, :viewmodel) | 
| 9 | 
            +
              Spec = Struct.new(:schema, :model, :viewmodel)
         | 
| 10 | 
            +
              class Spec
         | 
| 8 11 | 
             
                def initialize(schema:, model:, viewmodel:)
         | 
| 9 12 | 
             
                  super(schema, model, viewmodel)
         | 
| 10 13 | 
             
                end
         | 
| @@ -45,16 +48,16 @@ class ViewModel::TestHelpers::ARVMBuilder | |
| 45 48 | 
             
                  instance_eval(&block)
         | 
| 46 49 | 
             
                end
         | 
| 47 50 |  | 
| 48 | 
            -
                raise  | 
| 49 | 
            -
                raise  | 
| 50 | 
            -
                raise  | 
| 51 | 
            +
                raise 'Model not created in ARVMBuilder'     unless model
         | 
| 52 | 
            +
                raise 'Schema not created in ARVMBuilder'    unless model.table_exists?
         | 
| 53 | 
            +
                raise 'ViewModel not created in ARVMBuilder' unless viewmodel || @no_viewmodel
         | 
| 51 54 |  | 
| 52 55 | 
             
                # Force the realization of the view model into the library's lookup
         | 
| 53 56 | 
             
                # table. If this doesn't happen the library may have conflicting entries in
         | 
| 54 57 | 
             
                # the deferred table, and will allow viewmodels to leak between tests.
         | 
| 55 58 | 
             
                unless @no_viewmodel || !(@viewmodel < ViewModel::Record)
         | 
| 56 59 | 
             
                  resolved = ViewModel::Registry.for_view_name(viewmodel.view_name)
         | 
| 57 | 
            -
                  raise  | 
| 60 | 
            +
                  raise 'Failed to register expected new class!' unless resolved == @viewmodel
         | 
| 58 61 | 
             
                end
         | 
| 59 62 | 
             
              end
         | 
| 60 63 |  | 
| @@ -69,7 +72,7 @@ class ViewModel::TestHelpers::ARVMBuilder | |
| 69 72 | 
             
              private
         | 
| 70 73 |  | 
| 71 74 | 
             
              def viewmodel_name
         | 
| 72 | 
            -
                self.name +  | 
| 75 | 
            +
                self.name + 'View'
         | 
| 73 76 | 
             
              end
         | 
| 74 77 |  | 
| 75 78 | 
             
              def define_schema(&block)
         | 
| @@ -83,10 +86,11 @@ class ViewModel::TestHelpers::ARVMBuilder | |
| 83 86 |  | 
| 84 87 | 
             
              def define_model(&block)
         | 
| 85 88 | 
             
                model_name = name
         | 
| 86 | 
            -
                 | 
| 87 | 
            -
                @model = Class.new(@model_base) do | | 
| 88 | 
            -
                  raise "Model already defined: #{model_name}" if  | 
| 89 | 
            -
             | 
| 89 | 
            +
                model_namespace = namespace
         | 
| 90 | 
            +
                @model = Class.new(@model_base) do |_c|
         | 
| 91 | 
            +
                  raise "Model already defined: #{model_name}" if model_namespace.const_defined?(model_name, false)
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  model_namespace.const_set(model_name, self)
         | 
| 90 94 | 
             
                  class_eval(&block)
         | 
| 91 95 | 
             
                  reset_column_information
         | 
| 92 96 | 
             
                end
         | 
| @@ -95,13 +99,15 @@ class ViewModel::TestHelpers::ARVMBuilder | |
| 95 99 |  | 
| 96 100 | 
             
              def define_viewmodel(&block)
         | 
| 97 101 | 
             
                vm_name = viewmodel_name
         | 
| 98 | 
            -
                 | 
| 99 | 
            -
                @viewmodel = Class.new(@viewmodel_base) do | | 
| 100 | 
            -
                  raise "Viewmodel alreay defined: #{vm_name}" if  | 
| 101 | 
            -
             | 
| 102 | 
            +
                vm_namespace = namespace
         | 
| 103 | 
            +
                @viewmodel = Class.new(@viewmodel_base) do |_c|
         | 
| 104 | 
            +
                  raise "Viewmodel alreay defined: #{vm_name}" if vm_namespace.const_defined?(vm_name, false)
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  vm_namespace.const_set(vm_name, self)
         | 
| 102 107 | 
             
                  class_eval(&block)
         | 
| 103 108 | 
             
                end
         | 
| 104 | 
            -
                raise  | 
| 109 | 
            +
                raise 'help help' if @viewmodel.name.nil?
         | 
| 110 | 
            +
             | 
| 105 111 | 
             
                @viewmodel
         | 
| 106 112 | 
             
              end
         | 
| 107 113 |  |