iknow_view_models 2.8.4

Sign up to get free protection for your applications and to get access to all the features.
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'