iknow_view_models 2.8.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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +115 -0
  3. data/.gitignore +36 -0
  4. data/.travis.yml +31 -0
  5. data/Appraisals +9 -0
  6. data/Gemfile +19 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +19 -0
  9. data/Rakefile +21 -0
  10. data/appveyor.yml +22 -0
  11. data/gemfiles/rails_5_2.gemfile +15 -0
  12. data/gemfiles/rails_6_0_beta.gemfile +15 -0
  13. data/iknow_view_models.gemspec +49 -0
  14. data/lib/iknow_view_models.rb +12 -0
  15. data/lib/iknow_view_models/railtie.rb +8 -0
  16. data/lib/iknow_view_models/version.rb +5 -0
  17. data/lib/view_model.rb +333 -0
  18. data/lib/view_model/access_control.rb +154 -0
  19. data/lib/view_model/access_control/composed.rb +216 -0
  20. data/lib/view_model/access_control/open.rb +13 -0
  21. data/lib/view_model/access_control/read_only.rb +13 -0
  22. data/lib/view_model/access_control/tree.rb +264 -0
  23. data/lib/view_model/access_control_error.rb +10 -0
  24. data/lib/view_model/active_record.rb +383 -0
  25. data/lib/view_model/active_record/association_data.rb +178 -0
  26. data/lib/view_model/active_record/association_manipulation.rb +389 -0
  27. data/lib/view_model/active_record/cache.rb +265 -0
  28. data/lib/view_model/active_record/cache/cacheable_view.rb +51 -0
  29. data/lib/view_model/active_record/cloner.rb +113 -0
  30. data/lib/view_model/active_record/collection_nested_controller.rb +100 -0
  31. data/lib/view_model/active_record/controller.rb +77 -0
  32. data/lib/view_model/active_record/controller_base.rb +185 -0
  33. data/lib/view_model/active_record/nested_controller_base.rb +93 -0
  34. data/lib/view_model/active_record/singular_nested_controller.rb +34 -0
  35. data/lib/view_model/active_record/update_context.rb +252 -0
  36. data/lib/view_model/active_record/update_data.rb +749 -0
  37. data/lib/view_model/active_record/update_operation.rb +810 -0
  38. data/lib/view_model/active_record/visitor.rb +77 -0
  39. data/lib/view_model/after_transaction_runner.rb +29 -0
  40. data/lib/view_model/callbacks.rb +219 -0
  41. data/lib/view_model/changes.rb +62 -0
  42. data/lib/view_model/config.rb +29 -0
  43. data/lib/view_model/controller.rb +142 -0
  44. data/lib/view_model/deserialization_error.rb +437 -0
  45. data/lib/view_model/deserialize_context.rb +16 -0
  46. data/lib/view_model/error.rb +191 -0
  47. data/lib/view_model/error_view.rb +35 -0
  48. data/lib/view_model/record.rb +367 -0
  49. data/lib/view_model/record/attribute_data.rb +48 -0
  50. data/lib/view_model/reference.rb +31 -0
  51. data/lib/view_model/references.rb +48 -0
  52. data/lib/view_model/registry.rb +73 -0
  53. data/lib/view_model/schemas.rb +45 -0
  54. data/lib/view_model/serialization_error.rb +10 -0
  55. data/lib/view_model/serialize_context.rb +118 -0
  56. data/lib/view_model/test_helpers.rb +103 -0
  57. data/lib/view_model/test_helpers/arvm_builder.rb +111 -0
  58. data/lib/view_model/traversal_context.rb +126 -0
  59. data/lib/view_model/utils.rb +24 -0
  60. data/lib/view_model/utils/collections.rb +49 -0
  61. data/test/helpers/arvm_test_models.rb +59 -0
  62. data/test/helpers/arvm_test_utilities.rb +187 -0
  63. data/test/helpers/callback_tracer.rb +27 -0
  64. data/test/helpers/controller_test_helpers.rb +270 -0
  65. data/test/helpers/match_enumerator.rb +58 -0
  66. data/test/helpers/query_logging.rb +71 -0
  67. data/test/helpers/test_access_control.rb +56 -0
  68. data/test/helpers/viewmodel_spec_helpers.rb +326 -0
  69. data/test/unit/view_model/access_control_test.rb +769 -0
  70. data/test/unit/view_model/active_record/alias_test.rb +35 -0
  71. data/test/unit/view_model/active_record/belongs_to_test.rb +376 -0
  72. data/test/unit/view_model/active_record/cache_test.rb +351 -0
  73. data/test/unit/view_model/active_record/cloner_test.rb +313 -0
  74. data/test/unit/view_model/active_record/controller_test.rb +561 -0
  75. data/test/unit/view_model/active_record/counter_test.rb +80 -0
  76. data/test/unit/view_model/active_record/customization_test.rb +388 -0
  77. data/test/unit/view_model/active_record/has_many_test.rb +957 -0
  78. data/test/unit/view_model/active_record/has_many_through_poly_test.rb +269 -0
  79. data/test/unit/view_model/active_record/has_many_through_test.rb +736 -0
  80. data/test/unit/view_model/active_record/has_one_test.rb +334 -0
  81. data/test/unit/view_model/active_record/namespacing_test.rb +75 -0
  82. data/test/unit/view_model/active_record/optional_attribute_view_test.rb +58 -0
  83. data/test/unit/view_model/active_record/poly_test.rb +320 -0
  84. data/test/unit/view_model/active_record/shared_test.rb +285 -0
  85. data/test/unit/view_model/active_record/version_test.rb +121 -0
  86. data/test/unit/view_model/active_record_test.rb +542 -0
  87. data/test/unit/view_model/callbacks_test.rb +582 -0
  88. data/test/unit/view_model/deserialization_error/unique_violation_test.rb +73 -0
  89. data/test/unit/view_model/record_test.rb +524 -0
  90. data/test/unit/view_model/traversal_context_test.rb +371 -0
  91. data/test/unit/view_model_test.rb +62 -0
  92. metadata +490 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aae4e7d0b4370199361f2f7a44f12f4f8c30156e5b53990323f4bff8a1fc0daf
4
+ data.tar.gz: 0fb1fd1096212ffdd8b12693d1f411e910509d6e2b21e4d5b1b7208549db90f3
5
+ SHA512:
6
+ metadata.gz: 1ea33fb802b495ccc0040a1d6ea9652366ee3cd06d88c3e89bba9dec3fa8b960c30aaa7ef183b3b6224b905d4f56c43ae103ff86395f00711c742d38012fdcbd
7
+ data.tar.gz: 03d0e0aa61d6dca2bd270c803ce817817a80f8a26497ca6efaf6fe2e0a58c471b5d720f3f28081beba09b6798a3def23d13c1e1fff357e7cffb82a0a54ab0c06
@@ -0,0 +1,115 @@
1
+ version: 2.1
2
+
3
+ executors:
4
+ ruby-pg:
5
+ parameters:
6
+ ruby-version:
7
+ type: string
8
+ default: "2.6"
9
+ pg-version:
10
+ type: string
11
+ default: "11"
12
+ gemfile:
13
+ type: string
14
+ default: "Gemfile"
15
+ environment:
16
+ PGHOST: 127.0.0.1
17
+ PGUSER: eikaiwa
18
+ docker:
19
+ - image: circleci/ruby:<< parameters.ruby-version >>
20
+ environment:
21
+ BUNDLE_JOBS: 3
22
+ BUNDLE_RETRY: 3
23
+ BUNDLE_PATH: vendor/bundle
24
+ RAILS_ENV: test
25
+ BUNDLE_GEMFILE: << parameters.gemfile >>
26
+ - image: circleci/postgres:<< parameters.pg-version >>-alpine
27
+ environment:
28
+ POSTGRES_USER: eikaiwa
29
+ POSTGRES_DB: iknow_view_models
30
+ POSTGRES_PASSWORD: ""
31
+
32
+ jobs:
33
+ test:
34
+ parameters:
35
+ ex:
36
+ type: executor
37
+ executor: << parameters.ex >>
38
+ parallelism: 1
39
+ steps:
40
+ - checkout
41
+
42
+ - run:
43
+ # Remove the non-appraisal gemfile for safety: we never want to use it.
44
+ name: Prepare bundler
45
+ command: bundle -v && rm Gemfile
46
+
47
+ - run:
48
+ name: Compute a gemfile lock
49
+ command: bundle lock && cp "${BUNDLE_GEMFILE}.lock" /tmp/gem-lock
50
+
51
+ - restore_cache:
52
+ keys:
53
+ - iknow_viewmodels-{{ checksum "/tmp/gem-lock" }}
54
+ - iknow_viewmodels-
55
+
56
+ - run:
57
+ name: Bundle Install
58
+ command: bundle check || bundle install
59
+
60
+ - save_cache:
61
+ key: iknow_viewmodels-{{ checksum "/tmp/gem-lock" }}
62
+ paths:
63
+ - vendor/bundle
64
+
65
+ - run:
66
+ name: Wait for DB
67
+ command: dockerize -wait tcp://localhost:5432 -timeout 1m
68
+
69
+ - run:
70
+ name: Run minitest
71
+ command: bundle exec rake test
72
+
73
+ - store_test_results:
74
+ path: test/reports
75
+
76
+ publish:
77
+ executor: ruby-pg
78
+ steps:
79
+ - checkout
80
+ - run:
81
+ name: Setup Rubygems
82
+ command: |
83
+ mkdir ~/.gem &&
84
+ echo -e "---\r\n:rubygems_api_key: $RUBYGEMS_API_KEY" > ~/.gem/credentials &&
85
+ chmod 0600 ~/.gem/credentials
86
+ - run:
87
+ name: Publish to Rubygems
88
+ command: |
89
+ gem build iknow_view_models.gemspec
90
+ gem push iknow_view_models-*.gem
91
+
92
+ workflows:
93
+ version: 2.1
94
+ build:
95
+ jobs:
96
+ - test:
97
+ name: 'ruby 2.6 rails 5.2 pg 11'
98
+ ex:
99
+ name: ruby-pg
100
+ ruby-version: "2.6"
101
+ pg-version: "11"
102
+ gemfile: gemfiles/rails_5_2.gemfile
103
+ - test:
104
+ name: 'ruby 2.6 rails 6.0 pg 11'
105
+ ex:
106
+ name: ruby-pg
107
+ ruby-version: "2.6"
108
+ pg-version: "11"
109
+ gemfile: gemfiles/rails_6_0_beta.gemfile
110
+ - publish:
111
+ filters:
112
+ branches:
113
+ only: master
114
+ tags:
115
+ ignore: /.*/
@@ -0,0 +1,36 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .byebug_history
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ *.bundle
21
+ *.so
22
+ *.o
23
+ *.a
24
+ mkmf.log
25
+ *~
26
+ test/config/database.yml
27
+
28
+ # RubyMine
29
+ .idea/
30
+
31
+ # Appraisal
32
+ gemfiles/*.lock
33
+
34
+ # RVM
35
+ .ruby-version
36
+ .ruby-gemset
@@ -0,0 +1,31 @@
1
+ dist: trusty
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+
6
+ rvm:
7
+ - 2.5
8
+
9
+ gemfile:
10
+ - gemfiles/rails_5_2.gemfile
11
+
12
+ addons:
13
+ postgresql: "10"
14
+ apt:
15
+ packages:
16
+ - postgresql-10
17
+ - postgresql-client-10
18
+ - postgresql-server-dev-10
19
+ env:
20
+ global:
21
+ - PGPORT=5433
22
+
23
+ before_install:
24
+ - gem update --system
25
+ - gem install bundler
26
+
27
+ before_script:
28
+ - psql -c 'CREATE DATABASE iknow_view_models;'
29
+
30
+ notifications:
31
+ email: false
@@ -0,0 +1,9 @@
1
+ appraise "rails-5-2" do
2
+ gem "activerecord", "~> 5.2.0"
3
+ gem "activesupport", "~> 5.2.0"
4
+ end
5
+
6
+ appraise "rails-6-0-beta" do
7
+ gem "activerecord", "~> 6.0.0.beta"
8
+ gem "activesupport", "~> 6.0.0.beta"
9
+ end
data/Gemfile ADDED
@@ -0,0 +1,19 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cerego_view_models.gemspec
4
+ gemspec
5
+
6
+ # Test metadata collection for circleci
7
+ gem 'minitest-ci'
8
+
9
+ # Override gemspec for development version preferences
10
+ gem 'activerecord', '~> 5.1.0'
11
+ gem 'activesupport', '~> 5.1.0'
12
+
13
+ # Fetch private dependencies from git
14
+ gem 'acts_as_manual_list', git: 'https://github.com/iknow/acts_as_manual_list', branch: 'master'
15
+ gem 'deep_preloader', git: 'https://github.com/iknow/deep_preloader', branch: 'master'
16
+ gem 'iknow_cache', git: 'https://github.com/iknow/iknow_cache', branch: 'master'
17
+ gem 'iknow_params', '~> 2.2.0', git: 'https://github.com/iknow/iknow_params', branch: 'master'
18
+ gem 'keyword_builder', git: 'https://github.com/iknow/keyword_builder', branch: 'master'
19
+ gem 'safe_values', git: 'https://github.com/iknow/safe_values', branch: 'master'
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Chris Andreae
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ # IknowViewModels
2
+
3
+ [![Build Status](https://travis-ci.org/iknow/iknow_view_models.svg?branch=master)](https://travis-ci.org/iknow/iknow_view_models)
4
+
5
+ ViewModels provide a means of encapsulating a collection of related data and specifying its JSON serialization.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'iknow_view_models'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install iknow_view_models
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.pattern = 'test/**/*_test.rb'
7
+ t.warning = true
8
+ t.verbose = true
9
+ end
10
+
11
+ desc "Open an IRB console with the test helpers"
12
+ task :test_console do
13
+ ruby %{-r bundler/setup -Ilib -e 'load "test/helpers/arvm_test_models.rb"' -r irb -e 'IRB.start(__FILE__)'}
14
+ end
15
+
16
+ desc "Open a Pry console with the test helpers"
17
+ task 'test_console:pry' do
18
+ ruby %{-r bundler/setup -Ilib -e 'load "test/helpers/arvm_test_models.rb"' -r pry -e 'Pry.start'}
19
+ end
20
+
21
+ task :default => :test
@@ -0,0 +1,22 @@
1
+ ---
2
+ version: "{build}"
3
+ image: ubuntu
4
+
5
+ stack:
6
+ - ruby 2.5.0, postgresql
7
+
8
+ build: off
9
+
10
+ install:
11
+ - sudo -u postgres createdb -O appveyor iknow_view_models
12
+ - sudo apt-get update
13
+ - sudo apt-get -y install postgresql-server-dev-10
14
+ - gem install bundler
15
+ - bundle install
16
+
17
+ before_test:
18
+ - ruby -v
19
+ - bundle -v
20
+
21
+ test_script:
22
+ - bundle exec rake
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "minitest-ci"
6
+ gem "activerecord", "~> 5.2.0"
7
+ gem "activesupport", "~> 5.2.0"
8
+ gem "acts_as_manual_list", git: "https://github.com/iknow/acts_as_manual_list", branch: "master"
9
+ gem "deep_preloader", git: "https://github.com/iknow/deep_preloader", branch: "master"
10
+ gem "iknow_cache", git: "https://github.com/iknow/iknow_cache", branch: "master"
11
+ gem "iknow_params", "~> 2.2.0", git: "https://github.com/iknow/iknow_params", branch: "master"
12
+ gem "keyword_builder", git: "https://github.com/iknow/keyword_builder", branch: "master"
13
+ gem "safe_values", git: "https://github.com/iknow/safe_values", branch: "master"
14
+
15
+ gemspec path: "../"
@@ -0,0 +1,15 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "minitest-ci"
6
+ gem "activerecord", "~> 6.0.0.beta"
7
+ gem "activesupport", "~> 6.0.0.beta"
8
+ gem "acts_as_manual_list", git: "https://github.com/iknow/acts_as_manual_list", branch: "master"
9
+ gem "deep_preloader", git: "https://github.com/iknow/deep_preloader", branch: "master"
10
+ gem "iknow_cache", git: "https://github.com/iknow/iknow_cache", branch: "master"
11
+ gem "iknow_params", "~> 2.2.0", git: "https://github.com/iknow/iknow_params", branch: "master"
12
+ gem "keyword_builder", git: "https://github.com/iknow/keyword_builder", branch: "master"
13
+ gem "safe_values", git: "https://github.com/iknow/safe_values", branch: "master"
14
+
15
+ gemspec path: "../"
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ # coding: utf-8
3
+
4
+ lib = File.expand_path('../lib', __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require 'iknow_view_models/version'
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "iknow_view_models"
10
+ spec.version = IknowViewModels::VERSION
11
+ spec.authors = ["iKnow Team"]
12
+ spec.email = ["edge@iknow.jp"]
13
+ spec.summary = "ViewModels provide a means of encapsulating a collection of related data and specifying its JSON serialization."
14
+ spec.description = ""
15
+ spec.homepage = "https://github.com/iknow/cerego_view_models"
16
+ spec.license = "MIT"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "activerecord", ">= 5.0"
24
+ spec.add_dependency "activesupport", ">= 5.0"
25
+
26
+ spec.add_dependency "acts_as_manual_list"
27
+ spec.add_dependency "deep_preloader"
28
+ spec.add_dependency "iknow_cache"
29
+ spec.add_dependency "iknow_params"
30
+ spec.add_dependency "safe_values"
31
+ spec.add_dependency "keyword_builder"
32
+
33
+ spec.add_dependency "concurrent-ruby"
34
+ spec.add_dependency "jbuilder"
35
+ spec.add_dependency "json_schema"
36
+ spec.add_dependency "lazily"
37
+ spec.add_dependency "renum"
38
+
39
+ spec.add_development_dependency "appraisal"
40
+ spec.add_development_dependency "bundler"
41
+ spec.add_development_dependency "byebug"
42
+ spec.add_development_dependency "method_source"
43
+ spec.add_development_dependency "minitest-hooks"
44
+ spec.add_development_dependency "pg", '~> 0.18' # As of 5.1.4, Rails runtime check excludes pg 1.x, see #31669
45
+ spec.add_development_dependency "pry"
46
+ spec.add_development_dependency "rake"
47
+ spec.add_development_dependency "rspec-expectations"
48
+ spec.add_development_dependency "sqlite3"
49
+ end
@@ -0,0 +1,12 @@
1
+ require "iknow_view_models/version"
2
+ require "view_model"
3
+ require "view_model/controller"
4
+ require "view_model/active_record"
5
+ require "view_model/active_record/controller"
6
+ require "view_model/active_record/singular_nested_controller"
7
+ require "view_model/active_record/collection_nested_controller"
8
+
9
+ module IknowViewModels
10
+ end
11
+
12
+ require 'iknow_view_models/railtie' if defined?(Rails)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IknowViewModels::Railtie < Rails::Railtie
4
+ # On code reload, clear registered viewmodels that are no longer present.
5
+ config.to_prepare do
6
+ ViewModel::Registry.clear_removed_classes!
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IknowViewModels
4
+ VERSION = '2.8.4'
5
+ end
@@ -0,0 +1,333 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A ViewModel encapsulates a particular aggregation of data calculated via the
4
+ # underlying models and provides a means of serializing it into views.
5
+ require 'jbuilder'
6
+ require 'deep_preloader'
7
+
8
+ class ViewModel
9
+ REFERENCE_ATTRIBUTE = "_ref"
10
+ ID_ATTRIBUTE = "id"
11
+ TYPE_ATTRIBUTE = "_type"
12
+ VERSION_ATTRIBUTE = "_version"
13
+ NEW_ATTRIBUTE = "_new"
14
+
15
+ Metadata = Struct.new(:id, :view_name, :schema_version, :new) do
16
+ alias :new? :new
17
+ end
18
+
19
+ class << self
20
+ attr_accessor :_attributes
21
+ attr_accessor :schema_version
22
+ attr_reader :view_aliases
23
+
24
+ def inherited(subclass)
25
+ subclass.initialize_as_viewmodel
26
+ end
27
+
28
+ def initialize_as_viewmodel
29
+ @_attributes = []
30
+ @schema_version = 1
31
+ @view_aliases = []
32
+ end
33
+
34
+ def view_name
35
+ @view_name ||=
36
+ begin
37
+ # try to auto-detect based on class name
38
+ match = /(.*)View$/.match(self.name)
39
+ raise ArgumentError.new("Could not auto-determine ViewModel name from class name '#{self.name}'") if match.nil?
40
+ ViewModel::Registry.default_view_name(match[1])
41
+ end
42
+ end
43
+
44
+ def view_name=(name)
45
+ @view_name = name
46
+ end
47
+
48
+ def add_view_alias(as)
49
+ view_aliases << as
50
+ ViewModel::Registry.register(self, as: as)
51
+ end
52
+
53
+ # ViewModels are typically going to be pretty simple structures. Make it a
54
+ # bit easier to define them: attributes specified this way are given
55
+ # accessors and assigned in order by the default constructor.
56
+ def attributes(*attrs, **args)
57
+ attrs.each { |attr| attribute(attr, **args) }
58
+ end
59
+
60
+ def attribute(attr, **_args)
61
+ unless attr.is_a?(Symbol)
62
+ raise ArgumentError.new("ViewModel attributes must be symbols")
63
+ end
64
+
65
+ attr_accessor attr
66
+ define_method("deserialize_#{attr}") do |value, references: {}, deserialize_context: self.class.new_deserialize_context|
67
+ self.public_send("#{attr}=", value)
68
+ end
69
+ _attributes << attr
70
+ end
71
+
72
+ # An abstract viewmodel may want to define attributes to be shared by their
73
+ # subclasses. Redefine `_attributes` to close over the current class's
74
+ # _attributes and ignore children.
75
+ def lock_attribute_inheritance
76
+ _attributes.tap do |attrs|
77
+ define_singleton_method(:_attributes) { attrs }
78
+ attrs.freeze
79
+ end
80
+ end
81
+
82
+ def member_names
83
+ _attributes
84
+ end
85
+
86
+ # In deserialization, verify and extract metadata from a provided hash.
87
+ def extract_viewmodel_metadata(hash)
88
+ ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
89
+ id = hash.delete(ViewModel::ID_ATTRIBUTE)
90
+ type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
91
+ schema_version = hash.delete(ViewModel::VERSION_ATTRIBUTE)
92
+ new = hash.delete(ViewModel::NEW_ATTRIBUTE) { false }
93
+
94
+ Metadata.new(id, type_name, schema_version, new)
95
+ end
96
+
97
+ def extract_reference_only_metadata(hash)
98
+ ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
99
+ id = hash.delete(ViewModel::ID_ATTRIBUTE)
100
+ type_name = hash.delete(ViewModel::TYPE_ATTRIBUTE)
101
+
102
+ Metadata.new(id, type_name, nil, false)
103
+ end
104
+
105
+ def extract_reference_metadata(hash)
106
+ ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_REFERENCE, hash)
107
+ hash.delete(ViewModel::REFERENCE_ATTRIBUTE)
108
+ end
109
+
110
+ def is_update_hash?(hash)
111
+ ViewModel::Schemas.verify_schema!(ViewModel::Schemas::VIEWMODEL_UPDATE, hash)
112
+ hash.has_key?(ViewModel::ID_ATTRIBUTE) &&
113
+ !hash.fetch(ViewModel::ActiveRecord::NEW_ATTRIBUTE, false)
114
+ end
115
+
116
+ # If this viewmodel represents an AR model, what associations does it make
117
+ # use of? Returns a includes spec appropriate for DeepPreloader, either as
118
+ # AR-style nested hashes or DeepPreloader::Spec.
119
+ def eager_includes(serialize_context: new_serialize_context, include_shared: true)
120
+ {}
121
+ end
122
+
123
+ # ViewModel can serialize ViewModels, Arrays and Hashes of ViewModels, and
124
+ # relies on Jbuilder#merge! for other values (e.g. primitives).
125
+ def serialize(target, json, serialize_context: new_serialize_context)
126
+ case target
127
+ when ViewModel
128
+ target.serialize(json, serialize_context: serialize_context)
129
+ when Array
130
+ json.array! target do |elt|
131
+ serialize(elt, json, serialize_context: serialize_context)
132
+ end
133
+ when Hash, Struct
134
+ json.merge!({})
135
+ target.each_pair do |key, value|
136
+ json.set! key do
137
+ serialize(value, json, serialize_context: serialize_context)
138
+ end
139
+ end
140
+ else
141
+ json.merge! target
142
+ end
143
+ end
144
+
145
+ def serialize_as_reference(target, json, serialize_context: new_serialize_context)
146
+ if serialize_context.flatten_references
147
+ serialize(target, json, serialize_context: serialize_context)
148
+ else
149
+ ref = serialize_context.add_reference(target)
150
+ json.set!(REFERENCE_ATTRIBUTE, ref)
151
+ end
152
+ end
153
+
154
+ def serialize_to_hash(viewmodel, serialize_context: new_serialize_context)
155
+ Jbuilder.new { |json| serialize(viewmodel, json, serialize_context: serialize_context) }.attributes!
156
+ end
157
+
158
+ # Rebuild this viewmodel from a serialized hash.
159
+ def deserialize_from_view(hash_data, references: {}, deserialize_context: new_deserialize_context)
160
+ viewmodel = self.new
161
+ deserialize_members_from_view(viewmodel, hash_data, references: references, deserialize_context: deserialize_context)
162
+ viewmodel
163
+ end
164
+
165
+ def deserialize_members_from_view(viewmodel, view_hash, references:, deserialize_context:)
166
+ ViewModel::Callbacks.wrap_deserialize(viewmodel, deserialize_context: deserialize_context) do |hook_control|
167
+ if (bad_attrs = view_hash.keys - member_names).present?
168
+ causes = bad_attrs.map do |bad_attr|
169
+ ViewModel::DeserializationError::UnknownAttribute.new(bad_attr, viewmodel.blame_reference)
170
+ end
171
+ raise ViewModel::DeserializationError::Collection.for_errors(causes)
172
+ end
173
+
174
+ member_names.each do |attr|
175
+ if view_hash.has_key?(attr)
176
+ viewmodel.public_send("deserialize_#{attr}",
177
+ view_hash[attr],
178
+ references: references,
179
+ deserialize_context: deserialize_context)
180
+ end
181
+ end
182
+
183
+ deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, viewmodel)
184
+ viewmodel.validate!
185
+
186
+ # More complex viewmodels can use this hook to track changes to
187
+ # persistent backing models, and record the results. Primitive
188
+ # viewmodels record no changes.
189
+ if block_given?
190
+ yield(hook_control)
191
+ else
192
+ hook_control.record_changes(Changes.new)
193
+ end
194
+ end
195
+ end
196
+
197
+ def serialize_context_class
198
+ ViewModel::SerializeContext
199
+ end
200
+
201
+ def new_serialize_context(*args)
202
+ serialize_context_class.new(*args)
203
+ end
204
+
205
+ def deserialize_context_class
206
+ ViewModel::DeserializeContext
207
+ end
208
+
209
+ def new_deserialize_context(*args)
210
+ deserialize_context_class.new(*args)
211
+ end
212
+
213
+ def accepts_schema_version?(schema_version)
214
+ schema_version == self.schema_version
215
+ end
216
+
217
+ def preload_for_serialization(viewmodels, serialize_context: new_serialize_context, include_shared: true, lock: nil)
218
+ Array.wrap(viewmodels).group_by(&:class).each do |type, views|
219
+ DeepPreloader.preload(views.map(&:model),
220
+ type.eager_includes(serialize_context: serialize_context, include_shared: include_shared),
221
+ lock: lock)
222
+ end
223
+ end
224
+ end
225
+
226
+ def initialize(*args)
227
+ self.class._attributes.each_with_index do |attr, idx|
228
+ self.public_send(:"#{attr}=", args[idx])
229
+ end
230
+ end
231
+
232
+ # Serialize this viewmodel to a jBuilder by calling serialize_view. May be
233
+ # overridden in subclasses to (for example) implement caching.
234
+ def serialize(json, serialize_context: self.class.new_serialize_context)
235
+ ViewModel::Callbacks.wrap_serialize(self, context: serialize_context) do
236
+ serialize_view(json, serialize_context: serialize_context)
237
+ end
238
+ end
239
+
240
+ def to_hash(serialize_context: self.class.new_serialize_context)
241
+ Jbuilder.new { |json| serialize(json, serialize_context: serialize_context) }.attributes!
242
+ end
243
+
244
+ # Render this viewmodel to a jBuilder. Usually overridden in subclasses.
245
+ # Default implementation visits each attribute with Viewmodel.serialize.
246
+ def serialize_view(json, serialize_context: self.class.new_serialize_context)
247
+ self.class._attributes.each do |attr|
248
+ json.set! attr do
249
+ ViewModel.serialize(self.send(attr), json, serialize_context: serialize_context)
250
+ end
251
+ end
252
+ end
253
+
254
+ # ViewModels are often used to serialize ActiveRecord models. For convenience,
255
+ # if necessary we assume that the wrapped model is the first attribute. To
256
+ # change this, override this method.
257
+ def model
258
+ self.public_send(self.class._attributes.first)
259
+ end
260
+
261
+ # Provide a stable way to identify this view through attribute changes. By
262
+ # default views cannot make assumptions about the identity of our attributes,
263
+ # so we fall back on the view's `object_id`. If a viewmodel is backed by a
264
+ # model with a concept of identity, this method should be overridden to use
265
+ # it.
266
+ def id
267
+ object_id
268
+ end
269
+
270
+ # Is this viewmodel backed by a model with a stable identity? Used to decide
271
+ # whether the id is included when constructing a ViewModel::Reference from
272
+ # this view.
273
+ def stable_id?
274
+ false
275
+ end
276
+
277
+ def validate!; end
278
+
279
+ def to_reference
280
+ ViewModel::Reference.new(self.class, (id if stable_id?))
281
+ end
282
+
283
+ # Delegate view_name to class in most cases. Polymorphic views may wish to
284
+ # override this to select a specific alias.
285
+ def view_name
286
+ self.class.view_name
287
+ end
288
+
289
+ # When deserializing, if an error occurs within this viewmodel, what viewmodel
290
+ # is reported as to blame. Can be overridden for example when a viewmodel is
291
+ # merged with its parent.
292
+ def blame_reference
293
+ to_reference
294
+ end
295
+
296
+ def context_for_child(member_name, context:)
297
+ context.for_child(self, association_name: member_name)
298
+ end
299
+
300
+ def preload_for_serialization(lock: nil, serialize_context: self.class.new_serialize_context)
301
+ ViewModel.preload_for_serialization([self], lock: lock, serialize_context: serialize_context)
302
+ end
303
+
304
+ def ==(other)
305
+ other.class == self.class && self.class._attributes.all? do |attr|
306
+ other.send(attr) == self.send(attr)
307
+ end
308
+ end
309
+
310
+ alias eql? ==
311
+
312
+ def hash
313
+ features = self.class._attributes.map { |attr| self.send(attr) }
314
+ features << self.class
315
+ features.hash
316
+ end
317
+ end
318
+
319
+ require 'view_model/config'
320
+ require 'view_model/utils'
321
+ require 'view_model/error'
322
+ require 'view_model/callbacks'
323
+ require 'view_model/access_control'
324
+ require 'view_model/deserialization_error'
325
+ require 'view_model/serialization_error'
326
+ require 'view_model/registry'
327
+ require 'view_model/references'
328
+ require 'view_model/reference'
329
+ require 'view_model/serialize_context'
330
+ require 'view_model/deserialize_context'
331
+ require 'view_model/changes'
332
+ require 'view_model/schemas'
333
+ require 'view_model/error_view'