module Volt
Render the config/base/index.html when precompiling. Here we only render one js and one css file.
Collection helpers provide methods to access methods of page directly. @page is expected to be defined and a Volt::Page
The Inflector
transforms words from singular to plural, class names to table names, modularized class names to ones without, and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept in inflections.rb.
ModelChangeHelpers handle validating and persisting the data in a model when it is changed. run_changed will be called from the model.
Enforces a type on a field. Typically setup from “`field :name, Type“`
The BaseBinding
class is the base for all bindings. It takes 4 arguments that should be passed up from the children (via super)
-
page - this class instance should provide:
- a #templates methods that returns a hash for templates - an #events methods that returns an instance of DocumentEvents
-
target - an
DomTarget
orAttributeTarget
-
context - the context object the binding will be evaluated in
-
binding_name - the id for the comment (or id for attributes) where the
binding will be inserted.
Component bindings are the same as template bindings, but handle components.
Some template bindings share the controller with other template bindings based on a name. This class creates a cache based on the group_controller name and the controller class.
Initialize with the path to a component and returns all the front-end setup code (for controllers, models, views, and routes)
Volt
supports the concept of a message bus, a bus provides a pub/sub interface to any other volt instance (server, console, runner, etc..) inside a volt cluster. Volt
ships with a PeerToPeer message bus out of the box, but you can create or use other message bus's.
MessageBus
instances inherit from MessageBus::BaseMessageBus
and provide two methods 'publish' and 'subscribe'. They should be inside of Volt::MessageBus
.
publish should take a channel name and a message and deliver the message to any subscried listeners.
subscribe should take a channel name and a block. It should yield a message to the block if a message is published to the channel.
The implementation details of the pub/sub connection are left to the implemntation. If the user needs to configure server addresses, Volt.config
is the prefered location, so it can be configured from config/app.rb
MessageBus's should process their messages in their own thread. (And optionally may use a thread pool.)
You can use lib/volt/server/message_bus/message_encoder.rb for encoding and encryption if needed.
See lib/volt/server/message_bus/peer_to_peer.rb for details on volt's built-in message bus implementation.
NOTE: in the future, we plan to add support for round robbin message receiving and other patterns.
The message encoder handles reading/writing the message to/from the socket. This includes encrypting and formatting.
TODO: Right now the message bus uses threads, we should switch it to use a single thread and some form of select: practicingruby.com/articles/event-loops-demystified
Volt::MiddlewareStack
provides an interface where app code can add custom rack middleware. Volt.current_app
.middleware returns an instance of Volt::MiddlewareStack
, and apps can call use to add in more middleware.
Used to get a list of the assets and other included components from the dependencies.rb files.
Takes in the name and all component paths returns all of the ruby code for the component and its dependencies.
Serves the main pages
Sets up the maps for the opal assets, and source maps if enabled.
This file Monkeypatches sprockets to provide custom file loading (from volt instead disk) for component root files. These files then require in all parts or include generated ruby for templates, routes, and tasks.
DataTransformer
is a singleton class that walks ruby data structures (nested hashes, arrays, etc..) and lets you transform them based on values or keys
NOTE: DataTransformer
is not automatically required, but can be when needed.
The Actions module adds helpers for setting up and using actions on a class. You can setup helpers for an action with
setup_action_helpers_in_class(:before_action, :after_action)
The above will setup before_action and after_action methods on the class. Typically setup_action_helpers_in_class will be run in a base class.
before_action :require_login
The Volt::Repos
module provides access to each root collection (repo).
The Templates
class holds all loaded templates.
Attributes
Returns the config
Public Class Methods
as_user
lets you run a block as another user
@param user_or_user_id [Integer|Volt::Model]
# File lib/volt/volt/users.rb, line 43 def as_user(user_or_id) # if we have a user, get the id user_id = user_or_id.is_a?(Volt::Model) ? user_or_id.id : user_or_id previous_id = Thread.current['with_user_id'] Thread.current['with_user_id'] = user_id yield Thread.current['with_user_id'] = previous_id end
# File lib/volt/boot.rb, line 19 def self.boot(app_path) # Boot the app App.new(app_path) end
# File lib/volt.rb, line 37 def client? !ENV['SERVER'] end
When we use something like a Task
, we don't specify an app, so we use a thread local or global to lookup the current app. This lets us run more than one app at once, giving deference to a global app.
# File lib/volt.rb, line 67 def current_app Thread.current['volt_app'] || $volt_app end
Return the current user.
# File lib/volt/volt/users.rb, line 85 def current_user user_id = current_user_id if user_id Volt.current_app.store._users.where(id: user_id).first else Promise.new.resolve(nil) end end
True if the user is logged in and the user is loaded
# File lib/volt/volt/users.rb, line 78 def current_user? current_user.then do |user| !!user end end
Get the user_id from the cookie
# File lib/volt/volt/users.rb, line 6 def current_user_id # Check for a user_id from with_user if (user_id = Thread.current['with_user_id']) return user_id end user_id_signature = self.user_id_signature if user_id_signature.nil? nil else index = user_id_signature.index(':') # If no index, the cookie is invalid return nil unless index user_id = user_id_signature[0...index] if RUBY_PLATFORM != 'opal' hash = user_id_signature[(index + 1)..-1] # Make sure the user hash matches # TODO: We could cache the digest generation for even faster comparisons if hash != Digest::SHA256.hexdigest("#{Volt.config.app_secret}::#{user_id}") # user id has been tampered with, reject fail VoltUserError, 'user id or hash is incorrectly signed. It may have been tampered with, the app secret changed, or generated in a different app.' end end user_id end end
# File lib/volt/config.rb, line 49 def defaults app_name = File.basename(Dir.pwd) opts = { app_name: app_name, db_name: (ENV['DB_NAME'] || (app_name + '_' + Volt.env.to_s)).gsub('.', '_'), db_host: ENV['DB_HOST'] || 'localhost', db_port: (ENV['DB_PORT'] || 27_017).to_i, db_driver: ENV['DB_DRIVER'] || 'mongo', # a list of components which should be included in all components default_components: ['volt'], compress_javascript: Volt.env.production?, compress_css: Volt.env.production?, compress_images: Volt.env.production?, abort_on_exception: true, min_worker_threads: 1, max_worker_threads: 10, worker_timeout: 60 } opts[:db_uri] = ENV['DB_URI'] if ENV['DB_URI'] opts end
# File lib/volt.rb, line 50 def env @env ||= Volt::Environment.new end
# File lib/volt/volt/users.rb, line 100 def fetch_current_user Volt.logger.warn("Deprecation Warning: fetch current user have been depricated, Volt.current_user returns a promise now.") current_user end
Runs code in the context of this app.
# File lib/volt.rb, line 72 def in_app previous_app = Thread.current['volt_app'] Thread.current['volt_app'] = self begin yield ensure Thread.current['volt_app'] = previous_app end end
# File lib/volt.rb, line 60 def in_browser? @in_browser end
# File lib/volt.rb, line 54 def logger @logger ||= Volt::VoltLogger.new end
Login the user, return a promise for success
# File lib/volt/volt/users.rb, line 106 def login(username, password) UserTasks.login(login: username, password: password).then do |result| # Assign the user_id cookie for the user Volt.current_app.cookies._user_id = result # Pass nil back nil end end
# File lib/volt/volt/users.rb, line 116 def logout # Notify the backend so we can remove the user_id from the user's channel UserTasks.logout # Remove the cookie so user is no longer logged in Volt.current_app.cookies.delete(:user_id) end
Resets the configuration to the default (empty hash)
# File lib/volt/config.rb, line 77 def reset_config! configure do |c| c.from_h(defaults) end end
# File lib/volt.rb, line 26 def root fail 'Volt.root can not be called from the client.' if self.client? @root ||= File.expand_path(Dir.pwd) end
# File lib/volt.rb, line 33 def server? !!ENV['SERVER'] end
# File lib/volt/spec/capybara.rb, line 5 def setup_capybara(app_path, volt_app = nil) browser = ENV['BROWSER'] if browser setup_capybara_app(app_path, volt_app) case browser when 'phantom' Capybara.default_driver = :poltergeist when 'chrome', 'safari' # Use the browser name, note that safari requires an extension to run browser = browser.to_sym Capybara.register_driver(browser) do |app| Capybara::Selenium::Driver.new(app, browser: browser) end Capybara.default_driver = browser when 'firefox' Capybara.default_driver = :selenium when 'sauce' setup_sauce_labs end end end
# File lib/volt/spec/capybara.rb, line 30 def setup_capybara_app(app_path, volt_app) require 'capybara' require 'capybara/dsl' require 'capybara/rspec' require 'capybara/poltergeist' require 'selenium-webdriver' require 'volt/server' case RUNNING_SERVER when 'thin' Capybara.server do |app, port| require 'rack/handler/thin' Rack::Handler::Thin.run(app, Port: port) end when 'puma' Capybara.server do |app, port| Puma::Server.new(app).tap do |s| s.add_tcp_listener Capybara.server_host, port end.run.join end end # Setup server, use existing booted app Capybara.app = Server.new(app_path, volt_app).app end
Called on page load to pass the backend config to the client
# File lib/volt/config.rb, line 21 def setup_client_config(config_hash) # Only Volt.config.public is passed from the server (for security reasons) @config = wrap_config(public: config_hash) end
# File lib/volt/spec/sauce_labs.rb, line 3 def setup_sauce_labs require 'sauce' require 'sauce/capybara' Sauce.config do |c| if ENV['OS'] # Use a specifc OS, BROWSER, VERSION combo (for travis) c[:browsers] = [ [ENV['OS'], ENV['USE_BROWSER'], ENV['VERSION']] ] else # Run all c[:browsers] = [ # ["Windows 7", "Chrome", "30"], # ["Windows 8", "Firefox", "28"], ['Windows 8.1', 'Internet Explorer', '11'], ['Windows 8.0', 'Internet Explorer', '10'], ['Windows 7.0', 'Internet Explorer', '9'], # ["OSX 10.9", "iPhone", "8.1"], # ["OSX 10.8", "Safari", "6"], # ["Linux", "Chrome", "26"] ] end c[:start_local_application] = false end Capybara.default_driver = :sauce Capybara.javascript_driver = :sauce end
# File lib/volt/volt/users.rb, line 71 def skip_permissions Volt.run_in_mode(:skip_permissions) do yield end end
# File lib/volt.rb, line 41 def source_maps? if !ENV['MAPS'] # If no MAPS is specified, enable it in dev Volt.env.development? else ENV['MAPS'] != 'false' end end
# File lib/volt/spec/setup.rb, line 5 def spec_setup(app_path = '.') require 'volt' ENV['SERVER'] = 'true' ENV['VOLT_ENV'] = 'test' require 'volt/boot' # Create a main volt app for tests volt_app = Volt.boot(app_path) unless RUBY_PLATFORM == 'opal' begin require 'volt/spec/capybara' setup_capybara(app_path, volt_app) rescue LoadError => e Volt.logger.warn("unable to load capybara, if you wish to use it for tests, be sure it is in the app's Gemfile") Volt.logger.error(e) end end unless ENV['BROWSER'] # Not running integration tests with ENV['BROWSER'] RSpec.configuration.filter_run_excluding type: :feature end cleanup_db = -> do volt_app.database.drop_database # Clear cached for a reset volt_app.instance_variable_set('@store', nil) volt_app.reset_query_pool! end if RUBY_PLATFORM != 'opal' # Call once during setup to clear if we killed the last run cleanup_db.call end # Run everything in the context of this app Thread.current['volt_app'] = volt_app # Setup the spec collection accessors # RSpec.shared_context "volt collections", {} do RSpec.shared_context 'volt collections', {} do # Page conflicts with capybara's page method, so we call it the_page for now. # TODO: we need a better solution for page let(:the_page) { Model.new } let(:store) do @__store_accessed = true volt_app.store end let(:volt_app) { volt_app } let(:params) { volt_app.params } after do # Clear params if used url = volt_app.url if url.instance_variable_get('@params') url.instance_variable_set('@params', nil) end end if RUBY_PLATFORM != 'opal' after do |example| if @__store_accessed || example.metadata[:type] == :feature # Clear the database after each spec where we use store cleanup_db.call end end # Cleanup after integration tests also. before(:example, {type: :feature}) do @__store_accessed = true end end end end
Put in a deprecation placeholder
# File lib/volt/volt/users.rb, line 95 def user Volt.logger.warn('Deprecation: Volt.user has been renamed to Volt.current_user (to be more clear about what it returns). Volt.user will be deprecated in the future.') current_user end
Fetches the user_id+signature from the correct spot depending on client or server, does not verify it.
# File lib/volt/volt/users.rb, line 126 def user_id_signature if Volt.client? user_id_signature = Volt.current_app.cookies._user_id else # Check meta for the user id and validate it meta_data = Thread.current['meta'] if meta_data user_id_signature = meta_data['user_id'] else user_id_signature = nil end end user_id_signature end
Takes a user and returns a signed string that can be used for the user_id cookie to login a user.
# File lib/volt/volt/users.rb, line 58 def user_login_signature(user) fail 'app_secret is not configured' unless Volt.config.app_secret # TODO: returning here should be possible, but causes some issues # Salt the user id with the app_secret so the end user can't # tamper with the cookie signature = Digest::SHA256.hexdigest(salty_user_id(user.id)) # Return user_id:hash on user id "#{user.id}:#{signature}" end
Wraps the config hash in an OpenStruct
so it can be accessed in the same way as the server side config.
# File lib/volt/config.rb, line 28 def wrap_config(hash) new_hash = {} hash.each_pair do |key, value| if value.is_a?(Hash) new_hash[key] = wrap_config(value) else new_hash[key] = value end end OpenStruct.new(new_hash) end
Private Class Methods
# File lib/volt/volt/users.rb, line 145 def salty_user_id(user_id) "#{Volt.config.app_secret}::#{user_id}" end