lib/smooth/api.rb in smooth-2.0.1 vs lib/smooth/api.rb in smooth-2.0.2
- old
+ new
@@ -1,72 +1,326 @@
-require 'smooth/dsl'
-
+# The Smooth API
+#
+# An API is a collection of resources. A Resource is a collection of data models, and the API
+# allows us to run queries against those data models or to run commands that express an intent
+# to mutate the data models.
+#
+# An API provides different methods of authentication, and different policies for authorizations.
+#
+# An API is very easy to put a rest interface in front of, but can also work in other scenarios
+# that speak JSON since the interface pretty well encapsulates the behavior.
module Smooth
class Api
+ # Being able to inspect an API and produce data suitable for generating interface
+ # documentation, and automated tests, among other things, is a key feature of the gem.
include Smooth::Documentation
def self.default
- @default ||= Smooth::Api.new
+ @default ||= Smooth::Api.new(:default)
end
attr_accessor :name,
:_version_config,
:_resources,
:_policies
- def initialize(name, options={})
- @name = name
+ def initialize(name, options = {})
+ @name = name.to_s
@options = options
@_resources = {}
@_policies = {}
end
- def version config=nil
+ # The Smooth API is a gateway for the commands and queries
+ # that can be run by users against its resources
+ def lookup(path)
+ lookup_object_by(path)
+ end
+
+ # The Smooth API Provides a Rack Compatible interface
+ # so we can mount in sinatra or rails or whatever
+ def call(env)
+ sinatra.call(env)
+ end
+
+ # All Actions taken against the Smooth API are run 'as'
+ # some current user. Example:
+ #
+ # Running a command:
+ #
+ # api.as(jonathan).i_would_like_to
+ # .run_command("books.create").with(title:'Sup boo')
+ #
+ # Running a query
+ #
+ # api.as(soederpop).i_would_like_to
+ # .query("books").with(subject:"how to...")
+ #
+ def as(current_user, &block)
+ proxy = DslProxy.new(current_user, self)
+ proxy.instance_eval(&block) if block_given?
+ proxy
+ end
+
+ # The Smooth API generates a sinatra app to be able to
+ # the various resources and run commands, queries, etc.
+ def sinatra
+ app = @sinatra_application_klass ||= Class.new(Sinatra::Base)
+
+ @sinatra ||= begin
+ _resources.each do |_name, resource|
+ resource.router && resource.router.apply_to(app)
+ end
+
+ expose_interface_documentation_via(app)
+
+ app
+ end
+ end
+
+ def inspect
+ "Smooth API: #{ name } Resources: #{ resource_names }"
+ end
+
+ # The API will rely on the configured authentication method
+ # to determine who the user is. Given some request params
+ # and request headers
+ def lookup_current_user(params, headers)
+ auth_strategy, key = authentication_strategy
+
+ case
+ when auth_strategy == :param && parts = params[key]
+ user_class.find_for_token_authentication(parts)
+ when auth_strategy == :header && parts = headers[key]
+ user_class.find_for_token_authentication(parts)
+ else
+ user_class.anonymous(params, headers)
+ end
+ end
+
+ # The Policy will provide an ability file that we can
+ # run a user though. The Policy can be overridden by the
+ # resource, too. A policy will pass an object path
+ def lookup_policy(_params, _headers)
+ {}.to_mash
+ # TODO
+ #
+ # Implement:
+ #
+ # I think Smooth replaces too much of cancan to rely on it.
+ #
+ # I think the model where the resource inherits from the api, and
+ # the api policy just white lists or black lists commands for given
+ # user roles, will be sufficient
+ end
+
+ # The Smooth API provides an Asynchronous interface.
+ def perform_async(object_path, payload = {})
+ worker.perform_async serialize_for_async(object_path, payload)
+ end
+
+ # Takes a request to do something and serializes the arguments in
+ # the memory store. The request will be dispatched to the background job
+ # handler and then resumed with the same arguments.
+ #
+ # Note: Rails Global ID will be a good replacement for this
+ def serialize_for_async(object_path, payload)
+ key = "#{ name }".parameterize + ":cmd:#{ String.random_token(16) }"
+
+ request = {
+ api: name,
+ object_path: object_path,
+ payload: payload
+ }
+
+ Smooth.config.memory_store.write(key, request)
+
+ key
+ end
+
+ # Look up object by path. Used to route requests to
+ # commands or queries.
+ #
+ # Example:
+ #
+ # lookup('books.create') #=> CreateBook
+ def lookup_object_by(path)
+ path = path.to_s
+ resource_name, object_name = path.split(Smooth.config.object_path_separator)
+
+ resource_object = resource(resource_name)
+
+ case
+ when object_name == 'query' || object_name == 'serializer'
+ resource_object.fetch(object_name.to_sym, :default)
+ when object_name.nil?
+ resource_object
+ else
+ resource_object.fetch(:command, object_name)
+ end
+ end
+
+ def resource_keys
+ _resources.keys
+ end
+
+ def resource_names
+ _resources.values.map(&:resource_name).compact
+ end
+
+ def resource_group_names
+ _resources.values.map(&:group_description).compact
+ end
+
+ def documentation_base
+ {
+ api_meta: {
+ resource_names: resource_names,
+ resource_groups: resource_group_names
+ }
+ }
+ end
+
+ def interface_documentation
+ resource_keys.reduce(documentation_base) do |memo, key|
+ memo.tap do
+ if resource = resource(key)
+ memo[resource.resource_name || key.to_s] = resource.interface_documentation
+ end
+ end
+ end
+ end
+
+ def expose_interface_documentation_via(sinatra)
+ api = self
+
+ sinatra.send :get, '/interface' do
+ api.interface_documentation.to_json
+ end
+
+ sinatra.send :get, '/interface/:resource_name' do
+ docs = api.interface_documentation[params[:resource_name]]
+ docs.to_json
+ end
+ end
+
+ def version(config = nil)
@_version_config = config if config
@_version_config
end
- def policy policy_name, options={}, &block
+ def user_class(user_klass = nil, &block)
+ @user_class = user_klass if user_klass.present?
+ @user_class || User
+ @user_class.class_eval(&block) if block_given?
+ @user_class
+ end
+
+ def authentication_strategy(option = nil, key = nil)
+ return @authentication_strategy || [:header, 'X-AUTH-TOKEN'] if option.nil?
+
+ unless option.nil?
+ key = case
+ when key.present?
+ key
+ when option.to_sym == :param
+ :auth_token
+ when option.to_sym == :header
+ 'X-AUTH-TOKEN'
+ end
+ end
+
+ @authentication_strategy = [option, key]
+ end
+
+ def worker(&block)
+ worker_name = "#{ name }".camelize + 'Worker'
+
+ if worker_klass = Smooth::Api.const_get(worker_name) rescue nil
+ @worker_klass = worker_klass
+ else
+ Object.const_get(worker_name, @worker_klass = Class.new(Smooth::Command::AsyncWorker))
+ end
+
+ @worker_klass.instance_eval(&block)
+
+ @worker_klass
+ end
+
+ def policy(policy_name, options = {}, &block)
if obj = _policies[policy_name.to_sym]
obj.apply_options(options) unless options.empty?
obj.instance_eval(&block) if block_given?
obj
elsif options.empty? && !block_given?
nil
elsif block_given?
obj = Smooth::Api::Policy.new(policy_name, options, &block)
- _resources[policy_name.to_sym] = obj
+ _policies[policy_name.to_sym] = obj
end
end
- def has_resource? resource_name
- resources.has_key?(resource_name.to_sym)
+ def has_resource?(resource_name)
+ resources.key?(resource_name.to_sym)
end
- def resource resource_name, options={}, &block
- api_name = self.name
+ def resource(resource_name, options = {}, &block)
+ api_name = name
- if existing = _resources[resource_name.to_sym]
+ existing = _resources[resource_name.to_s.downcase]
+
+ if existing
existing.apply_options(options) unless options.empty?
existing.instance_eval(&block) if block_given?
existing
-
elsif options.empty? && !block_given?
existing = nil
elsif block_given?
created = Smooth::Resource.new(resource_name, options, &block).tap do |obj|
obj.api_name = api_name
end
- _resources[resource_name.to_sym] = created
+ _resources[resource_name.to_s.downcase] = created
end
end
+ end
+ class DslProxy
+ def initialize(current_user, api)
+ @current_user = current_user
+ @api = api
+ end
+
+ def i_would_like_to
+ self
+ end
+
+ def lemme
+ self
+ end
+
+ def imll
+ self
+ end
+
+ def query(resource_name, *args)
+ params = args.extract_options!
+ query_name = args.first || :default
+ runner = @api.resource(resource_name).fetch(:query, query_name).as(@current_user)
+ runner.async? ? perform_async(runner.object_path, params) : runner.run(params)
+ end
+
+ def run_command(resource_name, *args)
+ params = args.extract_options!
+ command_name = args.first
+ path = resource_name if command_name.nil?
+ path = "#{ resource_name }.#{ command_name }" if command_name.present?
+
+ runner = @api.lookup_object_by(path).as(@current_user)
+ runner.async? ? perform_async(runner.object_path, params) : runner.run(params)
+ end
end
end
-
-require 'smooth/api/tracking'
-require 'smooth/api/policy'