├── test ├── dummy │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── artist.rb │ │ │ └── song.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ ├── .keep │ │ │ │ └── logo.jpeg │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ └── stylesheets │ │ │ │ └── application.css.erb │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── index_controller.rb │ │ │ ├── application_controller.rb │ │ │ ├── songs_controller.rb │ │ │ └── musician_controller.rb │ │ ├── cells │ │ │ ├── song │ │ │ │ ├── song.css │ │ │ │ └── with_escaped.erb │ │ │ ├── form_tag_cell.rb │ │ │ ├── form_tag │ │ │ │ └── show.erb │ │ │ ├── form_for_cell.rb │ │ │ ├── form_for │ │ │ │ └── show.erb │ │ │ ├── simple_form │ │ │ │ └── show.erb │ │ │ ├── formtastic_cell.rb │ │ │ ├── formtastic │ │ │ │ └── show.erb │ │ │ ├── simple_form_cell.rb │ │ │ └── song_cell.rb │ │ ├── views │ │ │ ├── songs │ │ │ │ ├── show.html.erb │ │ │ │ └── with_escaped.html.erb │ │ │ ├── index │ │ │ │ └── index.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ └── helpers │ │ │ └── application_helper.rb │ ├── engines │ │ └── my_engine │ │ │ ├── app │ │ │ ├── assets │ │ │ │ ├── images │ │ │ │ │ └── my_engine │ │ │ │ │ │ └── .keep │ │ │ │ └── stylesheets │ │ │ │ │ └── my_engine │ │ │ │ │ └── application.css │ │ │ ├── concepts │ │ │ │ └── user │ │ │ │ │ ├── views │ │ │ │ │ ├── user.scss │ │ │ │ │ └── show.erb │ │ │ │ │ └── cell.rb │ │ │ ├── models │ │ │ │ └── my_engine │ │ │ │ │ └── user.rb │ │ │ ├── views │ │ │ │ ├── my_engine │ │ │ │ │ └── user │ │ │ │ │ │ └── show.html.erb │ │ │ │ └── layouts │ │ │ │ │ └── my_engine │ │ │ │ │ └── application.html.erb │ │ │ └── controllers │ │ │ │ └── my_engine │ │ │ │ ├── application_controller.rb │ │ │ │ └── user_controller.rb │ │ │ ├── .gitignore │ │ │ ├── README.rdoc │ │ │ ├── lib │ │ │ ├── my_engine │ │ │ │ ├── version.rb │ │ │ │ └── engine.rb │ │ │ ├── tasks │ │ │ │ └── my_engine_tasks.rake │ │ │ └── my_engine.rb │ │ │ ├── config │ │ │ └── routes.rb │ │ │ ├── db │ │ │ └── migrate │ │ │ │ └── 20150530135920_create_my_engine_users.rb │ │ │ ├── test │ │ │ └── fixtures │ │ │ │ └── my_engine │ │ │ │ └── users.yml │ │ │ ├── bin │ │ │ └── rails │ │ │ ├── Rakefile │ │ │ ├── Gemfile │ │ │ └── my_engine.gemspec │ ├── config │ │ ├── boot.rb │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── formtastic.rb │ │ │ ├── session_store.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── backtrace_silencers.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── routes.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── environments │ │ │ ├── development.rb │ │ │ └── test.rb │ │ ├── application.rb │ │ └── secrets.yml │ ├── Rakefile │ ├── test │ │ ├── integration │ │ │ ├── controller_test.rb │ │ │ ├── asset_pipeline_test.rb │ │ │ ├── form_tag_test.rb │ │ │ ├── form_for_test.rb │ │ │ ├── asset_helper_test.rb │ │ │ ├── url_helper_test.rb │ │ │ ├── simple_form_test.rb │ │ │ └── formtastic_test.rb │ │ ├── public_test.rb │ │ ├── context_test.rb │ │ ├── translation_test.rb │ │ ├── concept_generator_test.rb │ │ ├── test_helper.rb │ │ ├── rails_extensions_test.rb │ │ ├── cell_generator_test.rb │ │ └── caching_test.rb │ └── Gemfile └── test_helper.rb ├── .tool-versions ├── lib ├── cells-rails.rb ├── cells │ ├── rails │ │ └── version.rb │ └── rails.rb ├── rails │ └── generators │ │ ├── cell │ │ ├── templates │ │ │ ├── view.slim │ │ │ ├── view.haml │ │ │ ├── view.erb │ │ │ └── cell.rb.erb │ │ └── cell_generator.rb │ │ ├── concept │ │ ├── templates │ │ │ ├── view.slim │ │ │ ├── view.haml │ │ │ ├── view.erb │ │ │ └── concept.rb.erb │ │ └── concept_generator.rb │ │ └── test_unit │ │ ├── cell │ │ ├── templates │ │ │ └── unit_test.rb.erb │ │ └── cell_generator.rb │ │ └── concept │ │ ├── templates │ │ └── unit_test.rb.erb │ │ └── concept_generator.rb └── cell │ ├── rails │ ├── collection.rb │ ├── constant_for.rb │ └── testing.rb │ ├── test_case.rb │ ├── caching │ └── notification.rb │ ├── translation.rb │ ├── helper │ └── asset_helper.rb │ ├── railtie.rb │ └── rails.rb ├── .gitignore ├── Rakefile ├── Gemfile ├── gemfiles ├── 5.2.gemfile ├── 6.0.gemfile ├── 6.1.gemfile ├── 7.1.gemfile ├── 7.2.gemfile └── 8.0.gemfile ├── Appraisals ├── README.md ├── LICENSE.txt ├── CHANGES.md ├── cells-rails.gemspec └── .github └── workflows └── ci.yml /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 2.7.5 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/dummy/app/cells/song/song.css: -------------------------------------------------------------------------------- 1 | .song { background: red; } -------------------------------------------------------------------------------- /test/dummy/app/cells/song/with_escaped.erb: -------------------------------------------------------------------------------- 1 | <%= title %> -------------------------------------------------------------------------------- /test/dummy/app/views/songs/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= cell(:form_for) %> -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/app/assets/images/my_engine/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/cells-rails.rb: -------------------------------------------------------------------------------- 1 | require "cells/rails/version" 2 | require "cells/rails" 3 | -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link logo.jpeg 2 | //= link application.css 3 | -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/README.rdoc: -------------------------------------------------------------------------------- 1 | = MyEngine 2 | 3 | This project rocks and uses MIT-LICENSE. -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/app/concepts/user/views/user.scss: -------------------------------------------------------------------------------- 1 | user { 2 | background: green; 3 | } -------------------------------------------------------------------------------- /lib/cells/rails/version.rb: -------------------------------------------------------------------------------- 1 | module Cells 2 | module Rails 3 | VERSION = "0.1.6" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/lib/my_engine/version.rb: -------------------------------------------------------------------------------- 1 | module MyEngine 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/app/models/my_engine/user.rb: -------------------------------------------------------------------------------- 1 | module MyEngine 2 | class User 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/templates/view.slim: -------------------------------------------------------------------------------- 1 | h1 <%= class_name %>#<%= @state %> 2 | p Find me in <%= @path %> 3 | -------------------------------------------------------------------------------- /lib/rails/generators/concept/templates/view.slim: -------------------------------------------------------------------------------- 1 | h1 <%= class_name %>#<%= @state %> 2 | p Find me in <%= @path %> 3 | -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/app/concepts/user/views/show.erb: -------------------------------------------------------------------------------- 1 | <%= "Just an empty view, rendered by a cell..." %> 2 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/templates/view.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | <%= class_name %>#<%= @state %> 3 | %p 4 | Find me in <%= @path %> -------------------------------------------------------------------------------- /test/dummy/engines/my_engine/config/routes.rb: -------------------------------------------------------------------------------- 1 | MyEngine::Engine.routes.draw do 2 | get "user" => "user#show" 3 | end 4 | -------------------------------------------------------------------------------- /lib/rails/generators/concept/templates/view.haml: -------------------------------------------------------------------------------- 1 | %h1 2 | <%= class_name %>#<%= @state %> 3 | %p 4 | Find me in <%= @path %> -------------------------------------------------------------------------------- /test/dummy/app/cells/form_tag_cell.rb: -------------------------------------------------------------------------------- 1 | class FormTagCell < Cell::ViewModel 2 | def show 3 | render 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/dummy/app/views/songs/with_escaped.html.erb: -------------------------------------------------------------------------------- 1 |

Yeah!

<%= cell(:song, Song.new(title: "" 17 | end 18 | 19 | it do 20 | get :with, params: { song: { method: :stylesheet_link_tag, args: "application" } } 21 | _(response.body).must_include "href=\"http://assets.com/assets/application.css\"" 22 | _(response.body).must_include "media=\"screen\"" 23 | _(response.body).must_include "rel=\"stylesheet\"" 24 | end 25 | 26 | it do 27 | get :with, params: { song: { method: :asset_url, args: "application.css" } } 28 | _(response.body).must_equal "http://assets.com/assets/application.css" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/rails/generators/cell/cell_generator.rb: -------------------------------------------------------------------------------- 1 | module Rails 2 | module Generators 3 | class CellGenerator < NamedBase 4 | source_root File.expand_path('../templates', __FILE__) 5 | 6 | class_option :parent, type: :string, desc: 'The parent class for the generated cell' 7 | class_option :e, type: :string, desc: 'The template engine' 8 | 9 | check_class_collision suffix: 'Cell' 10 | 11 | argument :actions, type: :array, default: [], banner: 'action action2' 12 | 13 | def create_cell_file 14 | template 'cell.rb.erb', File.join('app/cells', class_path, "#{file_name}_cell.rb") 15 | end 16 | 17 | def create_view_files 18 | states.each do |state| 19 | @state = state 20 | @path = File.join('app/cells', class_path, file_name, "#{state}.#{template_engine}") 21 | template "view.#{template_engine}", @path 22 | end 23 | end 24 | 25 | hook_for :test_framework 26 | 27 | private 28 | 29 | def parent_class_name 30 | options[:parent] || 'Cell::ViewModel' 31 | end 32 | 33 | # The show state is included by default 34 | def states 35 | (['show'] + actions).uniq 36 | end 37 | 38 | def template_engine 39 | (options[:e] || Rails.application.config.app_generators.rails[:template_engine] || 'erb').to_s 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/musician_controller.rb: -------------------------------------------------------------------------------- 1 | class MusicianController < ActionController::Base 2 | def view_with_concept_with_show 3 | render inline: %{<%= concept("view_extensions_test/cell", "Up For Breakfast", volume: 1).show %>} # TODO: concept doesn't need .call 4 | end 5 | 6 | def view_with_concept_without_call 7 | render inline: %{<%= concept("view_extensions_test/cell", "A Tale That Wasn't Right") %>} # this tests ViewModel#to_s. 8 | end 9 | 10 | def view_with_concept_with_call 11 | render inline: %{<%= concept("view_extensions_test/cell", "A Tale That Wasn't Right").call %>} 12 | end 13 | 14 | def view_with_cell_with_call 15 | render inline: %{<%= cell("view_extensions_test/song", "A Tale That Wasn't Right").call %>} 16 | end 17 | 18 | def view_with_cell_with_call_keyword_args 19 | render inline: %{<%= cell("view_extensions_test/song", "A Tale That Wasn't Right").call(:with_title, with_title: 'test') %>} 20 | end 21 | 22 | def view_with_collection_without_call 23 | render inline: %{<%= cell("view_extensions_test/song", collection: ["A Tale That Wasn't Right"]) %>} 24 | end 25 | 26 | def action_with_concept_with_call 27 | render plain: concept("view_extensions_test/cell", "A Tale That Wasn't Right").call 28 | end 29 | 30 | def action_with_cell_with_call 31 | render plain: cell("view_extensions_test/song", "A Tale That Wasn't Right").call 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.1.6 2 | 3 | * Rails 8 is supported. 4 | * Ruby 3.4.0 is supported. 5 | 6 | ## 0.1.5 7 | 8 | * Support for Rails 7. 9 | * Migrate CI to Github Actions 10 | 11 | ## 0.1.4 12 | 13 | * Support Ruby 3.0. 14 | 15 | ## 0.1.3 16 | 17 | * Fix previous fixes. 18 | 19 | ## 0.1.2 20 | 21 | * Fix `LocalJumpError` from our Railtie. 22 | 23 | ## 0.1.1 24 | 25 | * The `Cell.view_paths` are now set automatically for `Trailblazer::Cell`s, too, if the gem is present. This allows starting Rails in daemon mode `rails s -d` and cells finding its views. 26 | 27 | ## 0.1.0 28 | 29 | * Support for Rails 6.0 Thank to @Szeliga 30 | 31 | ## 0.0.9 32 | 33 | * Prevent Forwardable from printing warnings in Ruby 2.4+ 34 | 35 | ## 0.0.8 36 | 37 | * Fix testing for `rspec-cells` in combination with Rails 5.1. Thanks @tiagoamaro. 38 | 39 | ## 0.0.7 40 | 41 | * Add `config.cells.include_template_engine = false` to turn off automatic 42 | inclusion of, e.g., `Cell::Slim`. 43 | * Require Cells >= 4.1.6. 44 | 45 | ## 0.0.6 46 | 47 | * `Testing` fixed with Rails 5. 48 | 49 | ## 0.0.5 50 | 51 | * Fix an annoying bundler loading bug. Thanks to @RKushnir. 52 | 53 | ## 0.0.4 54 | 55 | * Fix `Cell::Translation`. Thanks to @zavan! 56 | 57 | ## 0.0.3 58 | 59 | * `Cell::Helper::AssetHelper` is now always included and fixes https://github.com/apotonick/cells/issues/214 (hopefully). 60 | 61 | ## 0.0.2 62 | 63 | * Moved all Rails files from `cells` to `cells-rails`. 64 | -------------------------------------------------------------------------------- /test/dummy/test/integration/url_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class MusiciansController < ApplicationController 4 | end 5 | 6 | class WithDefaultOptionsController < ApplicationController 7 | private 8 | def default_url_options(options={}) 9 | options.merge(anchor: 'foobar') 10 | end 11 | end 12 | 13 | class UrlCell < Cell::ViewModel 14 | def show 15 | url_for(model) 16 | end 17 | end 18 | 19 | class UrlHelperTest < Minitest::Spec 20 | include Cell::Testing 21 | controller MusiciansController 22 | 23 | let (:song_cell) { UrlCell.new(Song.new, context: { controller: controller }) } 24 | 25 | # path helpers work in cell instance. 26 | it { _(song_cell.songs_path).must_equal "/songs" } 27 | it { _(song_cell.()).must_equal "http://test.host/songs/1" } 28 | end 29 | 30 | 31 | class UrlTest < ActionDispatch::IntegrationTest 32 | include ::Capybara::DSL 33 | 34 | it do 35 | visit "/songs/new" # cell.url_for(Song.new) 36 | _(page.text).must_equal "http://www.example.com/songs/1" 37 | end 38 | 39 | # it do 40 | # visit "/songs/1/edit" 41 | # page.text.must_equal "http://www.example.com/songs/1" 42 | # end 43 | end 44 | 45 | class DefaultOptionsTest < Minitest::Spec 46 | include Cell::Testing 47 | controller WithDefaultOptionsController 48 | 49 | let (:song_cell) { UrlCell.new(Song.new, context: { controller: controller }) } 50 | 51 | it { _(song_cell.songs_path).must_equal "/songs#foobar" } 52 | end 53 | -------------------------------------------------------------------------------- /test/dummy/test/translation_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "cell/translation" 3 | 4 | class TranslationTest < Minitest::Spec 5 | class I18nCell < Cell::ViewModel 6 | include ActionView::Helpers::TranslationHelper 7 | include Cell::Translation 8 | 9 | def greet_relative 10 | t(".greeting") # gets appended to translation_test.i18n. 11 | end 12 | 13 | def greet_absolute 14 | t("translation_test.i18n.greeting") 15 | end 16 | end 17 | 18 | I18n.backend = I18n::Backend::KeyValue.new({}) 19 | I18n.backend.store_translations(:en, 20 | { "translation_test.i18n.greeting" => "Translated!", 21 | "cell.friendly.greeting" => "Hello you!" }, 22 | escape: false) 23 | 24 | # Translate text specified by an absolute path 25 | it { _(I18nCell.new.greet_absolute).must_equal "Translated!" } 26 | 27 | # Translate text specified by an relative path 28 | it { _(I18nCell.new.greet_relative).must_equal "Translated!" } 29 | 30 | 31 | describe "::translation_path" do 32 | class ExplicitI18NCell < Cell::ViewModel 33 | include ActionView::Helpers::TranslationHelper 34 | include Cell::Translation 35 | 36 | self.translation_path = "cell.friendly" 37 | 38 | def show 39 | t(".greeting") 40 | end 41 | end 42 | 43 | it { _(ExplicitI18NCell.new.()).must_equal "Hello you!" } 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /cells-rails.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'cells/rails/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'cells-rails' 9 | spec.version = Cells::Rails::VERSION 10 | spec.authors = ['Nick Sutterer'] 11 | spec.email = ['apotonick@gmail.com'] 12 | spec.license = 'MIT' 13 | 14 | spec.summary = 'Convenient Rails support for Cells.' 15 | spec.homepage = 'https://trailblazer.to' 16 | 17 | spec.metadata['homepage_uri'] = spec.homepage 18 | spec.metadata['source_code_uri'] = 'https://github.com/trailblazer/cells-rails' 19 | spec.metadata['bug_tracker_uri'] = "#{spec.metadata['source_code_uri']}/issues" 20 | spec.metadata['changelog_uri'] = "#{spec.metadata['source_code_uri']}/blob/HEAD/CHANGES.md" 21 | spec.metadata['documentation_uri'] = 'https://trailblazer.to/2.1/docs/cells#cells-rails' 22 | 23 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|\.github)/}) } 24 | spec.require_paths = ['lib'] 25 | 26 | spec.add_dependency 'actionpack', '>= 5.0' 27 | spec.add_dependency 'cells', '>= 4.1.6', '< 5.0.0' 28 | 29 | spec.add_development_dependency 'appraisal' 30 | spec.add_development_dependency 'bundler' 31 | spec.add_development_dependency 'cells-erb' 32 | spec.add_development_dependency 'minitest' 33 | spec.add_development_dependency 'rails' 34 | spec.add_development_dependency 'rake' 35 | spec.add_development_dependency 'sass-rails' 36 | spec.add_development_dependency 'debug' 37 | end 38 | -------------------------------------------------------------------------------- /test/dummy/test/concept_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/generators/test_case' 3 | require 'rails/generators/concept/concept_generator' 4 | 5 | class ConceptGeneratorTest < Rails::Generators::TestCase 6 | destination File.expand_path('../../tmp', File.dirname(__FILE__)) 7 | setup :prepare_destination 8 | tests Rails::Generators::ConceptGenerator 9 | 10 | test '[erb] standard assets, show view' do 11 | run_generator %w(song -e erb) 12 | 13 | assert_file 'app/concepts/song/cell.rb', /class Song::Cell < Cell::Concept/ 14 | assert_file 'app/concepts/song/cell.rb', /def show/ 15 | assert_file 'app/concepts/song/views/show.erb', %r{app/concepts/song/views/show\.erb} 16 | end 17 | 18 | test "test unit test" do 19 | run_generator %w(song -t test_unit) 20 | 21 | # file contains #concept(..) call. 22 | assert_file 'test/concepts/song/cell_test.rb', /concept\("song\/cell"\).\(:show\)/ 23 | end 24 | 25 | test '[haml] standard assets, show view' do 26 | run_generator %w(song -e haml) 27 | 28 | assert_file 'app/concepts/song/cell.rb', /class Song::Cell < Cell::Concept/ 29 | assert_file 'app/concepts/song/cell.rb', /def show/ 30 | assert_file 'app/concepts/song/views/show.haml', %r{app/concepts/song/views/show\.haml} 31 | end 32 | 33 | test '[slim] standard assets, show view' do 34 | run_generator %w(song -e slim) 35 | 36 | assert_file 'app/concepts/song/cell.rb', /class Song::Cell < Cell::Concept/ 37 | assert_file 'app/concepts/song/cell.rb', /def show/ 38 | assert_file 'app/concepts/song/views/show.slim', %r{app/concepts/song/views/show\.slim} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/cell/helper/asset_helper.rb: -------------------------------------------------------------------------------- 1 | module Cell 2 | module Helper 3 | # Delegate all asset-related helpers to the global helpers instance. 4 | # This is the cleanest solution to leverage Rails' asset management and 5 | # doesn't pollute your cell with weird asset modules from Rails. 6 | module AssetHelper 7 | # Extend if we forgot anything. 8 | # This delegates asset helpers to the global Rails helper instance. 9 | 10 | # http://api.rubyonrails.org/classes/ActionView/Helpers/AssetUrlHelper.html 11 | %w{ 12 | javascript_include_tag 13 | stylesheet_link_tag 14 | 15 | asset_path 16 | asset_url 17 | image_tag 18 | video_tag 19 | audio_path 20 | audio_url 21 | compute_asset_extname 22 | compute_asset_host 23 | compute_asset_path 24 | favicon_link_tag 25 | font_path 26 | font_url 27 | image_path 28 | image_url 29 | javascript_path 30 | javascript_url 31 | path_to_asset 32 | path_to_audio 33 | path_to_font 34 | path_to_image 35 | path_to_javascript 36 | path_to_stylesheet 37 | path_to_video 38 | stylesheet_path 39 | stylesheet_url 40 | url_to_asset 41 | url_to_audio 42 | url_to_font 43 | url_to_image 44 | url_to_javascript 45 | url_to_stylesheet 46 | url_to_video 47 | video_path 48 | video_url 49 | }.each do |method| 50 | define_method(method) do |*args| 51 | ::ActionController::Base.helpers.send(method, *args) 52 | end 53 | end 54 | end # AssetHelper 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/dummy/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] = 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | # Minitest::Spec.class_eval do 6 | # after :each do 7 | # # DatabaseCleaner.clean 8 | # Thing.delete_all 9 | # Comment.delete_all 10 | # User.delete_all 11 | # end 12 | # end 13 | 14 | class MusicianController < ActionController::Base 15 | def view_with_concept_with_show 16 | render inline: %{<%= concept("view_extensions_test/cell", "Up For Breakfast", volume: 1).show %>} # TODO: concept doesn't need .call 17 | end 18 | 19 | def view_with_concept_without_call 20 | render inline: %{<%= concept("view_extensions_test/cell", "A Tale That Wasn't Right") %>} # this tests ViewModel#to_s. 21 | end 22 | 23 | def view_with_concept_with_call 24 | render inline: %{<%= concept("view_extensions_test/cell", "A Tale That Wasn't Right").call %>} 25 | end 26 | 27 | def view_with_cell_with_call 28 | render inline: %{<%= cell("view_extensions_test/song", "A Tale That Wasn't Right").call %>} 29 | end 30 | 31 | def view_with_cell_with_call_keyword_args 32 | render inline: %{<%= cell("view_extensions_test/song", "A Tale That Wasn't Right").call(:with_title, with_title: 'test') %>} 33 | end 34 | 35 | def view_with_collection_without_call 36 | render inline: %{<%= cell("view_extensions_test/song", collection: ["A Tale That Wasn't Right"]) %>} 37 | end 38 | 39 | def action_with_concept_with_call 40 | render plain: concept("view_extensions_test/cell", "A Tale That Wasn't Right").call 41 | end 42 | 43 | def action_with_cell_with_call 44 | render plain: cell("view_extensions_test/song", "A Tale That Wasn't Right").call 45 | end 46 | end 47 | 48 | Rails.backtrace_cleaner.remove_silencers! # YES, stacktraces are awesome! 49 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | config.serve_static_files = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Randomize the order test cases are executed. 30 | config.active_support.test_order = :random 31 | 32 | # Print deprecation notices to the stderr. 33 | config.active_support.deprecation = :stderr 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | 38 | config.action_controller.asset_host = "http://assets.com" 39 | # config.assets.compile = false 40 | 41 | # # Generate digests for assets URLs. 42 | # config.assets.digest = true 43 | end 44 | -------------------------------------------------------------------------------- /test/dummy/test/integration/simple_form_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SimpleFormTest < Minitest::Spec 4 | include Cell::Testing 5 | controller SongsController # provides #url_options. 6 | 7 | it do 8 | if Rails.version >= "6.0" 9 | _(cell("simple_form").().gsub(/\s\s/, "")).must_equal %{
First
} 10 | else 11 | _(cell("simple_form").().gsub(/\s\s/, "")).must_equal("
First
") 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy/test/rails_extensions_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ViewExtensionsTest < ActionController::TestCase 4 | tests MusicianController 5 | 6 | class Cell < Cell::Concept 7 | def show 8 | "#{model}" 9 | end 10 | 11 | def with_title(with_title: '') 12 | "

#{with_title}

#{model}" 13 | end 14 | end 15 | 16 | class SongCell < ::Cell::ViewModel 17 | def show 18 | "#{model}" 19 | end 20 | 21 | def with_title(options) 22 | "

#{options[:with_title]}

#{model}" 23 | end 24 | end 25 | 26 | 27 | # #concept is available in controller views. 28 | test "concept(..).show" do 29 | get :view_with_concept_with_show 30 | assert_equal "<b>Up For Breakfast</b>", @response.body # TODO: test options/with twin. 31 | end 32 | 33 | test "concept(..).call" do 34 | get :view_with_concept_with_call 35 | assert_equal "A Tale That Wasn't Right", @response.body # TODO: test options/with twin. 36 | end 37 | 38 | test "concept(..) without #call" do 39 | get :view_with_concept_without_call 40 | assert_equal "A Tale That Wasn't Right", @response.body 41 | end 42 | 43 | test "cell(collection: ..) without #call" do 44 | get :view_with_collection_without_call 45 | assert_equal "A Tale That Wasn't Right", @response.body 46 | end 47 | 48 | test "cell(..) with #call" do 49 | get :view_with_cell_with_call 50 | assert_equal "A Tale That Wasn't Right", @response.body 51 | end 52 | 53 | test "cell(..) with #call keyword args" do 54 | get :view_with_cell_with_call_keyword_args 55 | assert_equal "

test

A Tale That Wasn't Right", @response.body 56 | end 57 | 58 | # Controller#concept 59 | test "Controller#concept(..).call" do 60 | get :action_with_concept_with_call 61 | assert_equal "A Tale That Wasn't Right", @response.body # TODO: test options/with twin. 62 | end 63 | 64 | # Controller#cell 65 | test "Controller#cell(..).call" do 66 | get :action_with_cell_with_call 67 | assert_equal "A Tale That Wasn't Right", @response.body # TODO: test options/with twin. 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/dummy/test/integration/formtastic_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FormForTestTest < Minitest::Spec 4 | include Cell::Testing 5 | controller SongsController # provides #url_options. 6 | 7 | it do 8 | if Rails.version >= "6.0" 9 | _(cell("formtastic").().gsub(/\s\s/, "")).must_equal %{
First
    } 10 | else 11 | _(cell("formtastic").().gsub(/\s\s/, "")).must_equal %{
    First
    } 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | strategy: 6 | fail-fast: false 7 | matrix: 8 | # Due to https://github.com/actions/runner/issues/849, we have to use quotes for '3.0' 9 | ruby: [2.5, 2.6, 2.7, '3.0', 3.1, 3.2, 3.3, head] 10 | gemfile: 11 | - gemfiles/8.0.gemfile 12 | - gemfiles/7.2.gemfile 13 | - gemfiles/7.1.gemfile 14 | - gemfiles/6.0.gemfile 15 | - gemfiles/5.2.gemfile 16 | exclude: 17 | - ruby: 2.5 18 | gemfile: gemfiles/7.2.gemfile 19 | - ruby: 2.5 20 | gemfile: gemfiles/7.1.gemfile 21 | - ruby: 2.5 22 | gemfile: gemfiles/8.0.gemfile 23 | - ruby: 2.6 24 | gemfile: gemfiles/7.2.gemfile 25 | - ruby: 2.6 26 | gemfile: gemfiles/7.1.gemfile 27 | - ruby: 2.6 28 | gemfile: gemfiles/8.0.gemfile 29 | - ruby: 2.7 30 | gemfile: gemfiles/7.2.gemfile 31 | - ruby: 2.7 32 | gemfile: gemfiles/7.1.gemfile 33 | - ruby: 2.7 34 | gemfile: gemfiles/8.0.gemfile 35 | - ruby: 3.0 36 | gemfile: gemfiles/5.2.gemfile 37 | - ruby: 3.0 38 | gemfile: gemfiles/8.0.gemfile 39 | - ruby: 3.0 40 | gemfile: gemfiles/7.2.gemfile 41 | - ruby: 3.1 42 | gemfile: gemfiles/5.2.gemfile 43 | - ruby: 3.1 44 | gemfile: gemfiles/6.0.gemfile 45 | - ruby: 3.1 46 | gemfile: gemfiles/8.0.gemfile 47 | - ruby: 3.2 48 | gemfile: gemfiles/5.2.gemfile 49 | - ruby: 3.2 50 | gemfile: gemfiles/6.0.gemfile 51 | - ruby: 3.3 52 | gemfile: gemfiles/5.2.gemfile 53 | - ruby: 3.3 54 | gemfile: gemfiles/6.0.gemfile 55 | - ruby: head 56 | gemfile: gemfiles/5.2.gemfile 57 | - ruby: head 58 | gemfile: gemfiles/6.0.gemfile 59 | 60 | runs-on: ubuntu-latest 61 | env: 62 | BUNDLE_GEMFILE: ${{ matrix.gemfile }} 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | - uses: ruby/setup-ruby@v1 67 | with: 68 | ruby-version: ${{ matrix.ruby }} 69 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 70 | - run: bundle exec rake 71 | -------------------------------------------------------------------------------- /lib/cell/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails/railtie" 2 | require "cell/rails" 3 | 4 | module Cell 5 | class Railtie < Rails::Railtie 6 | 7 | config.cells = ActiveSupport::OrderedOptions.new 8 | 9 | initializer("cells.attach_router") do |app| 10 | ViewModel.class_eval do 11 | include app.routes.url_helpers # TODO: i hate this, make it better in Rails. 12 | end 13 | end 14 | 15 | initializer "cells.update_view_paths" do |app| 16 | # Add Rails.root to view_paths 17 | Cell::ViewModel.view_paths = [Rails.root.join("app", "cells")] 18 | end 19 | 20 | config.after_initialize do |app| 21 | Array(app.config.cells.with_assets).each do |cell_class| 22 | app.config.assets.paths += cell_class.camelize.constantize.prefixes # Song::Cell.prefixes 23 | end 24 | end 25 | 26 | initializer "cells.rails_extensions" do |app| 27 | ActiveSupport.on_load(:action_controller) do 28 | self.class_eval do 29 | include ::Cell::RailsExtensions::ActionController 30 | end 31 | end 32 | 33 | ActiveSupport.on_load(:action_view) do 34 | self.class_eval do 35 | include ::Cell::RailsExtensions::ActionView 36 | end 37 | end 38 | 39 | require "cell/rails/collection" 40 | require "cell/rails/constant_for" 41 | 42 | Cell::Collection.send :include, Cell::RailsExtension::Collection 43 | Cell::ViewModel.send :include, Cell::RailsExtension::ConstantFor 44 | end 45 | 46 | initializer "cells.include_default_helpers" do 47 | # include asset helpers (image_path, font_path, ect) 48 | ViewModel.class_eval do 49 | include ActionView::Helpers::FormHelper # includes ActionView::Helpers::UrlHelper, ActionView::Helpers::FormTagHelper 50 | include ::Cell::RailsExtensions::HelpersAreShit 51 | 52 | require "cell/helper/asset_helper" 53 | include Cell::Helper::AssetHelper 54 | 55 | # set VM#cache_store, etc. 56 | include RailsExtensions::ViewModel 57 | end 58 | end 59 | 60 | IncludeTemplateModules = ->(app) do 61 | return if app.config.cells.include_template_engine == false 62 | 63 | # yepp, this is happening. saves me a lot of coding in each extension. 64 | ViewModel.send(:include, Cell::Erb) if Cell.const_defined?(:Erb, false) 65 | ViewModel.send(:include, Cell::Haml) if Cell.const_defined?(:Haml, false) 66 | ViewModel.send(:include, Cell::Hamlit) if Cell.const_defined?(:Hamlit, false) 67 | ViewModel.send(:include, Cell::Slim) if Cell.const_defined?(:Slim, false) 68 | end 69 | 70 | initializer( "cells.include_template_module", after: "cells.include_default_helpers", &IncludeTemplateModules) 71 | 72 | initializer("cells.development") do |app| 73 | if Rails.env == "development" 74 | require "cell/development" 75 | ViewModel.send(:include, Development) 76 | end 77 | end 78 | 79 | initializer "trailblazer.cells.update_view_paths" do |app| 80 | if Object.const_defined?(:Trailblazer) && Trailblazer.const_defined?(:Cell, false) 81 | # Add Rails.root to view_paths 82 | Trailblazer::Cell.view_paths = [Rails.root.join("app", "concepts")] 83 | end 84 | end 85 | 86 | rake_tasks do 87 | load "tasks/cells.rake" 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/dummy/test/cell_generator_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | require 'rails/generators/test_case' 3 | require 'rails/generators/cell/cell_generator' 4 | 5 | class CellGeneratorTest < Rails::Generators::TestCase 6 | tests Rails::Generators::CellGenerator 7 | destination File.expand_path('../../tmp', File.dirname(__FILE__)) 8 | setup :prepare_destination 9 | 10 | test 'create the standard assets' do 11 | run_generator %w(blog post latest -e erb) 12 | 13 | assert_file 'app/cells/blog_cell.rb', /class BlogCell < Cell::ViewModel/ 14 | assert_file 'app/cells/blog_cell.rb', /def post/ 15 | assert_file 'app/cells/blog_cell.rb', /def latest/ 16 | assert_file 'app/cells/blog/post.erb', %r{app/cells/blog/post\.erb} 17 | assert_file 'app/cells/blog/post.erb', %r{

    } 18 | assert_file 'app/cells/blog/latest.erb', %r{app/cells/blog/latest\.erb} 19 | end 20 | 21 | test 'create cell that inherits from custom cell class if specified' do 22 | run_generator %w(Blog --parent=ApplicationCell) 23 | assert_file 'app/cells/blog_cell.rb', /class BlogCell < ApplicationCell/ 24 | end 25 | 26 | test 'work with namespaces' do 27 | run_generator %w(blog/post latest -e erb) 28 | assert_file 'app/cells/blog/post_cell.rb', /class Blog::PostCell < Cell::ViewModel/ 29 | assert_file 'app/cells/blog/post_cell.rb', /def show/ 30 | assert_file 'app/cells/blog/post_cell.rb', /def latest/ 31 | assert_file 'app/cells/blog/post/latest.erb', %r{app/cells/blog/post/latest\.erb} 32 | end 33 | 34 | test 'work with namespaces and haml' do 35 | run_generator %w(blog/post latest -e haml) 36 | assert_file 'app/cells/blog/post_cell.rb', /class Blog::PostCell < Cell::ViewModel/ 37 | assert_file 'app/cells/blog/post/latest.haml', %r{app/cells/blog/post/latest\.haml} 38 | end 39 | 40 | test 'work with namespaces and slim' do 41 | run_generator %w(blog/post latest -e slim) 42 | 43 | assert_file 'app/cells/blog/post_cell.rb', /class Blog::PostCell < Cell::ViewModel/ 44 | assert_file 'app/cells/blog/post/latest.slim', %r{app/cells/blog/post/latest\.slim} 45 | end 46 | 47 | test 'create slim assets with -e slim' do 48 | run_generator %w(blog post latest -e slim) 49 | 50 | assert_file 'app/cells/blog_cell.rb', /class BlogCell < Cell::ViewModel/ 51 | assert_file 'app/cells/blog_cell.rb', /def post/ 52 | assert_file 'app/cells/blog_cell.rb', /def latest/ 53 | assert_file 'app/cells/blog/post.slim', %r{app/cells/blog/post\.slim} 54 | assert_file 'app/cells/blog/post.slim', %r{p} 55 | assert_file 'app/cells/blog/latest.slim', %r{app/cells/blog/latest\.slim} 56 | end 57 | 58 | test 'create haml assets with -e haml' do 59 | run_generator %w(Blog post latest -e haml) 60 | 61 | assert_file 'app/cells/blog_cell.rb', /class BlogCell < Cell::ViewModel/ 62 | assert_file 'app/cells/blog_cell.rb', /def post/ 63 | assert_file 'app/cells/blog_cell.rb', /def latest/ 64 | assert_file 'app/cells/blog/post.haml', %r{app/cells/blog/post\.haml} 65 | assert_file 'app/cells/blog/post.haml', %r{%p} 66 | assert_file 'app/cells/blog/latest.haml', %r{app/cells/blog/latest\.haml} 67 | end 68 | 69 | test 'create test_unit assets with -t test_unit' do 70 | run_generator %w(Blog post latest -t test_unit) 71 | 72 | # file is there. 73 | assert_file 'test/cells/blog_cell_test.rb' 74 | # file contains #cell(..) call. 75 | assert_file 'test/cells/blog_cell_test.rb', /cell\("blog"\).\(:post\)/ 76 | end 77 | 78 | test 'work with namespace and test_unit' do 79 | run_generator %w(blog/post latest -t test_unit) 80 | 81 | assert_file 'test/cells/blog/post_cell_test.rb', /class Blog::PostCellTest < Cell::TestCase/ 82 | end 83 | 84 | test 'create test_unit assets with -t rspec' do 85 | run_generator %w(Blog post latest -t rspec) 86 | 87 | assert_no_file 'test/cells/blog_cell_test.rb' 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /lib/cell/rails.rb: -------------------------------------------------------------------------------- 1 | require 'active_support' 2 | require 'active_support/concern' 3 | module Cell 4 | def self.rails_version 5 | Gem::Version.new(ActionPack::VERSION::STRING) 6 | end 7 | 8 | # These methods are automatically added to all controllers and views. 9 | module RailsExtensions 10 | module ActionController 11 | def cell(name, model=nil, options={}, constant=::Cell::ViewModel, &block) 12 | options[:context] ||= {} 13 | options[:context][:controller] = self 14 | 15 | constant.cell(name, model, options, &block) 16 | end 17 | 18 | def concept(name, model=nil, options={}, &block) 19 | cell(name, model, options, ::Cell::Concept, &block) 20 | end 21 | end 22 | 23 | module ActionView 24 | # Returns the cell instance for +name+. You may pass arbitrary options to your 25 | # cell. 26 | # 27 | # = cell(:song, title: "Creeping Out Sara").(:show) 28 | def cell(name, *args, &block) 29 | controller.cell(name, *args, &block) 30 | end 31 | 32 | # # See Cells::Rails::ActionController#render_cell. 33 | # def render_cell(name, state, *args, &block) 34 | # ::Cell::Rails.render_cell(name, state, controller, *args, &block) 35 | # end 36 | 37 | def concept(name, *args, &block) 38 | controller.concept(name, *args, &block) 39 | end 40 | end 41 | 42 | # Gets included into Cell::ViewModel in a Rails environment. 43 | module ViewModel 44 | extend ActiveSupport::Concern 45 | 46 | # DISCUSS: who actually uses forgery protection with cells? it is not working since 4, anyway? 47 | # include ActionController::RequestForgeryProtection 48 | included do 49 | extend Uber::Delegates 50 | delegates :parent_controller, :session, :params, :request, :config, :env, :url_options, :default_url_options 51 | # forgery protection. 52 | delegates :parent_controller, :request_forgery_protection_token 53 | end 54 | 55 | def call(*) 56 | super.html_safe 57 | end 58 | 59 | def parent_controller 60 | context[:controller] 61 | end 62 | alias_method :controller, :parent_controller 63 | 64 | def perform_caching? 65 | ::ActionController::Base.perform_caching 66 | end 67 | 68 | def cache_store # we want to use DI to set a cache store in cell/rails. 69 | ::ActionController::Base.cache_store 70 | end 71 | 72 | # In Ruby 2.4.0+, Forwardable prints a warning when you delegate 73 | # to a private or protected method - so `delegates :protect_against_forgery?` 74 | # or `delegates :form_authenticity_token` will print warnings all 75 | # over the place 76 | # 77 | # This workaround prevents warnings being printed 78 | def protect_against_forgery? 79 | controller.send(:protect_against_forgery?) 80 | end 81 | 82 | def form_authenticity_token(**options) 83 | controller.send(:form_authenticity_token, **options) 84 | end 85 | 86 | module ClassMethods 87 | def expand_cache_key(key) 88 | ::ActiveSupport::Cache.expand_cache_key(key, :cells) 89 | end 90 | end 91 | end 92 | 93 | # In Rails, there are about 10 different implementations of #url_for. Rails doesn't like the idea of objects, so they 94 | # have helpers in modules. Those module are now included sequentially into other modules and/or classes. While they 95 | # get included, they might or might not include methods, depending on the including module/class 96 | # (example here: https://github.com/rails/rails/blob/cad20f720c4c6e04584253cd0a23f22b3d43ab0f/actionpack/lib/action_dispatch/routing/url_for.rb#L87). 97 | # 98 | # The outcome is that several module override #url_for, and if you're lucky, this works. If you're not, then #url_for breaks 99 | # due to a raise in one of its basic implementations, introduced in 3.x, fixed in 4.0 and then re-introduced in 4.2 100 | # 101 | # This is extremely frustrating as no one in Rails core seems to tackle this problem and introduces a url object instead 102 | # of this module madness. I have to constantly test and fix it in Cells. With the module below, I'll stop doing this. 103 | # 104 | # Either Rails works with Cells and we fix this in form of a URL object that gets passed into the cell (I'm happy with 105 | # a global object here, too! Wow!) or URL helpers will stop working in Cells and a lot of people will be unhappy. 106 | # 107 | # Anyway, this is the reason we need this patch module. If you have trouble with URLs in Cells, then please ask Rails to 108 | # fix their implementation. Thank you. 109 | module HelpersAreShit 110 | def url_for(options = nil) # from ActionDispatch:R:UrlFor. 111 | case options 112 | when nil 113 | _routes.url_for(url_options.symbolize_keys) 114 | when Hash 115 | _routes.url_for(options.symbolize_keys.reverse_merge!(url_options)) 116 | when String 117 | options 118 | when Array 119 | polymorphic_url(options, options.extract_options!) 120 | else 121 | polymorphic_url(options) 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /test/dummy/test/caching_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'test_helper' 3 | 4 | class DirectorCell < Cell::ViewModel 5 | attr_reader :count 6 | 7 | def initialize(*) 8 | super 9 | @count = 0 10 | end 11 | 12 | cache :tock 13 | 14 | def tock 15 | @count += 1 16 | end 17 | end 18 | 19 | 20 | class CachingUnitTest < Minitest::Spec 21 | before :each do 22 | ActionController::Base.cache_store.clear 23 | ActionController::Base.perform_caching = true 24 | end 25 | 26 | let (:director) { DirectorCell } 27 | let (:cellule) { DirectorCell.new(nil) } 28 | 29 | 30 | describe "::state_cache_key" do 31 | # accepts state name, only. 32 | it { _(director.state_cache_key(:count)).must_equal "cells/director/count/" } 33 | 34 | # accepts hash as key parts 35 | if Cell.rails_version >= Gem::Version.new('4.0') 36 | it { _(director.state_cache_key(:count, b: 2, a: 1)).must_equal "cells/director/count/b/2/a/1" } 37 | else 38 | it { _(director.state_cache_key(:count, b: 2, a: 1)).must_equal "cells/director/count/a=1&b=2" } 39 | end 40 | 41 | # accepts array as key parts 42 | it { _(director.state_cache_key(:count, [1, 2, 3])).must_equal "cells/director/count/1/2/3" } 43 | 44 | # accepts string as key parts 45 | it { _(director.state_cache_key(:count, "1/2")).must_equal "cells/director/count/1/2" } 46 | 47 | # accepts nil as key parts 48 | it { _(director.state_cache_key(:count, nil)).must_equal "cells/director/count/" } 49 | end 50 | 51 | 52 | describe "#state_cached?" do 53 | # true for cached 54 | it { _(cellule.send(:state_cached?, :tock)).must_equal true } 55 | 56 | # false otherwise 57 | it { _(cellule.send(:state_cached?, :sing)).must_equal false } 58 | end 59 | 60 | 61 | describe "#cache?" do 62 | # true for cached 63 | it { _(cellule.cache?(:tock)).must_equal true } 64 | 65 | # false otherwise 66 | it { _(cellule.cache?(:sing)).must_equal false } 67 | 68 | describe "perform_caching turned off" do 69 | after do 70 | ::ActionController::Base.perform_caching = true 71 | end 72 | 73 | # always false 74 | it do 75 | ::ActionController::Base.perform_caching = false 76 | _(cellule.cache?(:sing)).must_equal false 77 | _(cellule.cache?(:sing)).must_equal false 78 | end 79 | end 80 | 81 | describe "#cache_store" do 82 | # rails cache store per default. 83 | it { _(cellule.cache_store).must_equal ActionController::Base.cache_store } 84 | end 85 | end 86 | 87 | 88 | # describe ".expire_cache_key" do 89 | # before :each do 90 | # @key = @class.state_cache_key(:tock) 91 | # puts "====== key is #{@key}" 92 | # assert_equal "1", render_cell(:director, :tock) 93 | # assert_equal "1", @class.cache_store.read(@key) 94 | # end 95 | 96 | # it "delete the state from cache" do 97 | # @class.expire_cache_key(@key) 98 | # assert_not @class.cache_store.read(@key) 99 | # end 100 | 101 | # it "be available in controllers for sweepers" do 102 | # MusicianController.new.expire_cell_state(DirectorCell, :tock) 103 | # assert_not @class.cache_store.read(@key) 104 | # end 105 | 106 | # it "accept cache options" do 107 | # key = @class.state_cache_key(:tock, volume: 9) 108 | # assert Cell::Rails.cache_store.write(key, 'ONE!') 109 | 110 | # MusicianController.new.expire_cell_state(DirectorCell, :tock, volume: 9) 111 | # assert_equal "1", @class.cache_store.read(@key) 112 | # assert_not ::Cell::Rails.cache_store.read(key) 113 | # end 114 | # end 115 | end 116 | 117 | 118 | class CachingTest < Minitest::Spec 119 | class DirectorCell < Cell::ViewModel 120 | def initialize(counter=0) 121 | super 122 | @counter = counter 123 | end 124 | 125 | def show # public method. 126 | @counter 127 | end 128 | 129 | 130 | cache :utf8 131 | 132 | def utf8 133 | "æøå" # or any other UTF-8 string 134 | end 135 | end 136 | 137 | before :each do 138 | ActionController::Base.cache_store.clear 139 | ActionController::Base.perform_caching = true 140 | end 141 | 142 | # let (:cell) { DirectorCell.new(nil) } 143 | def director_cell(*args) 144 | DirectorCell.new(*args) 145 | end 146 | 147 | # no caching when turned off. 148 | it do 149 | director_cell.class.cache :show 150 | ActionController::Base.perform_caching = false 151 | 152 | _(director_cell(1).call).must_equal "1" 153 | _(director_cell(2).call).must_equal "2" 154 | end 155 | 156 | # cache forever when no options. 157 | it do 158 | director_cell.class.cache :show 159 | _(director_cell(1).call).must_equal "1" 160 | _(director_cell(2).call).must_equal "1" 161 | end 162 | 163 | # caches with string state name. 164 | it do 165 | director_cell.class.cache :show 166 | _(director_cell(1).("show")).must_equal "1" 167 | _(director_cell(2).("show")).must_equal "1" 168 | end 169 | 170 | 171 | # no caching when state not configured. 172 | it do 173 | director_cell.class.class_eval do 174 | def dictate 175 | @counter 176 | end 177 | end 178 | 179 | _(director_cell(1).call(:dictate)).must_equal "1" 180 | _(director_cell(2).call(:dictate)).must_equal "2" 181 | end 182 | 183 | # compute key with cell properties from #initialize. 184 | it do 185 | director_cell.class.cache :show do 186 | @counter < 3 ? {count: "<"} : {count: ">"} 187 | end 188 | 189 | _(director_cell(1).call).must_equal "1" 190 | _(director_cell(2).call).must_equal "1" 191 | _(director_cell(3).call).must_equal "3" 192 | _(director_cell(4).call).must_equal "3" 193 | end 194 | 195 | # compute key with instance method 196 | it do 197 | director_cell.class.cache :show, :version 198 | director_cell.class.class_eval do 199 | def version 200 | @counter < 3 ? {count: "<"} : {count: ">"} 201 | end 202 | end 203 | 204 | _(director_cell(1).call).must_equal "1" 205 | _(director_cell(2).call).must_equal "1" 206 | _(director_cell(3).call).must_equal "3" 207 | _(director_cell(4).call).must_equal "3" 208 | end 209 | 210 | # allow returning strings for key 211 | it do 212 | director_cell.class.cache :show do 213 | @counter < 3 ? "<" : ">" 214 | end 215 | 216 | _(director_cell(1).call).must_equal "1" 217 | _(director_cell(2).call).must_equal "1" 218 | _(director_cell(3).call).must_equal "3" 219 | _(director_cell(4).call).must_equal "3" 220 | end 221 | 222 | # allows conditional ifs. 223 | it do 224 | director_cell.class.cache :show, if: lambda { @counter < 3 } 225 | 226 | _(director_cell(1).call).must_equal "1" 227 | _(director_cell(2).call).must_equal "1" 228 | _(director_cell(3).call).must_equal "3" 229 | _(director_cell(4).call).must_equal "4" 230 | end 231 | 232 | # allows conditional ifs with instance method. 233 | it do 234 | director_cell.class.class_eval do 235 | cache :show, if: :smaller? 236 | 237 | def smaller? 238 | @counter < 3 239 | end 240 | end 241 | 242 | _(director_cell(1).call).must_equal "1" 243 | _(director_cell(2).call).must_equal "1" 244 | _(director_cell(3).call).must_equal "3" 245 | _(director_cell(4).call).must_equal "4" 246 | end 247 | 248 | 249 | unless ::ActionPack::VERSION::MAJOR == 3 and ::ActionPack::VERSION::MINOR >= 2 # bug in 3.2. 250 | describe "utf-8" do 251 | before do 252 | @key = director_cell.class.state_cache_key(:utf8) 253 | end 254 | 255 | it "has the correct encoding when reading from cache" do 256 | assert_equal "UTF-8", director_cell.call(:utf8).encoding.to_s 257 | assert_equal "UTF-8", director_cell.cache_store.read(@key).encoding.to_s 258 | end 259 | end 260 | end 261 | 262 | # options are passed through to cache store. 263 | # :expires_in. 264 | # tags: lambda { |one, two, three| "#{one},#{two},#{three}" } 265 | class CacheStore 266 | attr_reader :fetch_args 267 | 268 | def fetch(*args) 269 | @fetch_args = args 270 | end 271 | end 272 | 273 | it do 274 | cellule = self.director_cell 275 | 276 | cellule.instance_eval do 277 | def cache_store; 278 | @cache_store ||= CacheStore.new; 279 | end 280 | end 281 | 282 | cellule.class.cache :show, expires_in: 1.minutes, tags: lambda { self.class.to_s } 283 | cellule.call 284 | _(cellule.cache_store.fetch_args).must_equal ["cells/caching_test/director/show/", {expires_in: 60, tags: "CachingTest::DirectorCell"}] 285 | end 286 | end 287 | 288 | 289 | class CachingInheritanceTest < CachingTest 290 | class DirectorCell < ::DirectorCell 291 | cache :show, expires_in: 10.minutes do 292 | "v1" 293 | end 294 | end 295 | 296 | class DirectorsSonCell < DirectorCell 297 | end 298 | 299 | class DirectorsDaughterCell < ::DirectorCell 300 | cache :show, expires_in: 9.minutes do 301 | "v2" 302 | end 303 | end 304 | 305 | end 306 | --------------------------------------------------------------------------------