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)

  1. page - this class instance should provide:

    - a #templates methods that returns a hash for templates
    - an #events methods that returns an instance of DocumentEvents
    
  2. target - an DomTarget or AttributeTarget

  3. context - the context object the binding will be evaluated in

  4. 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

config[R]

Returns the config

logger[W]
root[W]

Public Class Methods

as_user(user_or_id) { || ... } click to toggle source

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
boot(app_path) click to toggle source
# File lib/volt/boot.rb, line 19
def self.boot(app_path)
  # Boot the app
  App.new(app_path)
end
client?() click to toggle source
# File lib/volt.rb, line 37
def client?
  !ENV['SERVER']
end
current_app() click to toggle source

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
current_user() click to toggle source

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
current_user?() click to toggle source

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
current_user_id() click to toggle source

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
defaults() click to toggle source
# 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
env() click to toggle source
# File lib/volt.rb, line 50
def env
  @env ||= Volt::Environment.new
end
fetch_current_user() click to toggle source
# 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
in_app() { || ... } click to toggle source

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
in_browser?() click to toggle source
# File lib/volt.rb, line 60
def in_browser?
  @in_browser
end
logger() click to toggle source
# File lib/volt.rb, line 54
def logger
  @logger ||= Volt::VoltLogger.new
end
login(username, password) click to toggle source

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
logout() click to toggle source
# 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
reset_config!() click to toggle source

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
root() click to toggle source
# 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
server?() click to toggle source
# File lib/volt.rb, line 33
def server?
  !!ENV['SERVER']
end
setup_capybara(app_path, volt_app = nil) click to toggle source
# 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
setup_capybara_app(app_path, volt_app) click to toggle source
# 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
setup_client_config(config_hash) click to toggle source

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
setup_sauce_labs() click to toggle source
# 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
skip_permissions() { || ... } click to toggle source
# File lib/volt/volt/users.rb, line 71
def skip_permissions
  Volt.run_in_mode(:skip_permissions) do
    yield
  end
end
source_maps?() click to toggle source
# 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
spec_setup(app_path = '.') click to toggle source
# 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
user() click to toggle source

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
user_id_signature() click to toggle source

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
user_login_signature(user) click to toggle source

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
wrap_config(hash) click to toggle source

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

salty_user_id(user_id) click to toggle source
# File lib/volt/volt/users.rb, line 145
def salty_user_id(user_id)
  "#{Volt.config.app_secret}::#{user_id}"
end