app/models/setup/algorithm.rb
require 'rkelly'
module Setup
class Algorithm
include SnippetCode
include NamespaceNamed
include Taggable
# = Algorithm
#
# Is a core concept in Cenit, define function that is possible execute.
legacy_code_attribute :code
trace_include :code
build_in_data_type.referenced_by(:namespace, :name).and(
properties: {
language: {
enum: %w(auto ruby javascript),
enumNames: ['Auto detect', 'Ruby', 'JavaScript'],
default: 'auto'
}
}
)
field :description, type: String
embeds_many :parameters, class_name: Setup::AlgorithmParameter.to_s, inverse_of: :algorithm
embeds_many :call_links, class_name: Setup::CallLink.to_s, inverse_of: :algorithm
validates_format_of :name, with: /\A[a-z]([a-z]|_|\d)*\Z/
accepts_nested_attributes_for :parameters, allow_destroy: true
accepts_nested_attributes_for :call_links, allow_destroy: true
field :store_output, type: Mongoid::Boolean
belongs_to :output_datatype, class_name: Setup::DataType.to_s, inverse_of: nil
field :validate_output, type: Mongoid::Boolean
field :parameters_size, type: Integer
before_save :validate_parameters, :validate_code, :validate_output_processing
attr_reader :last_output
field :language, type: StringifiedSymbol, default: -> { new_record? ? :auto : :ruby }
validates_inclusion_of :language, in: ->(alg) { alg.class.language_enum.values }
def required_parameters_size
parameters.to_a.count(&:required)
end
def code_extension
case language
when :python
'.py'
when :javascript
'.js'
when :php
'.php'
else
'.rb'
end
end
def validate_parameters
not_required = false
parameters.each do |p|
next unless not_required ||= !p.required
p.errors.add(:required, 'marked as "Required" must come before non marked') if p.required
end
errors.add(:parameters, 'contains invalid sequence of required parameters') if (last = parameters.last) && last.errors.present?
self.parameters_size = parameters.size
abort_if_has_errors
end
def validate_code
if code.blank?
errors.add(:code, "can't be blank")
else
logs = parse_code
if logs[:errors].present?
logs[:errors].each { |msg| errors.add(:code, msg) }
self.call_links = []
else
links = []
(logs[:self_sends] || []).each do |call_name|
if (call_link = call_links.where(name: call_name).first)
links << call_link
else
links << Setup::CallLink.new(name: call_name)
end
end
self.call_links = links
do_link
end
end
abort_if_has_errors
end
def validate_output_processing
if store_output && !output_datatype
rc = Setup::FileDataType.find_or_create_by(namespace: namespace, name: "#{name} output")
if rc.errors.present?
errors.add(:output_datatype, rc.errors.full_messages)
else
self.output_datatype = rc
end
end
abort_if_has_errors
end
def do_link
call_links.each(&:do_link)
end
attr_accessor :self_linker
def with_linker(linker)
self.self_linker = linker
self
end
def do_store_output(output)
rc = []
r = nil
while output.capataz_proxy?
output = output.capataz_slave
end
if output_datatype.is_a? Setup::FileDataType
begin
case output
when Hash, Array
r = output_datatype.create_from!(output.to_json, contentType: 'application/json')
when String
ct = 'text/plain'
begin
JSON.parse(output)
ct = 'application/json'
rescue JSON::ParserError
unless Nokogiri.XML(output).errors.present?
ct = 'application/xml'
end
end
r = output_datatype.create_from!(output, contentType: ct)
else
r = output_datatype.create_from!(output.to_s)
end
rescue Exception
r = output_datatype.create_from!(output.to_s)
end
else
begin
case output
when Hash, String
begin
r = output_datatype.create_from_json!(output)
rescue Exception => e
puts e.backtrace
end
when Array
output.each do |item|
rc += do_store_output(item)
end
else
fail
end
rescue Exception
fail 'Output failed to validate against Output DataType.'
end
end
if r
if r.errors.present?
fail 'Output failed to validate against Output DataType.'
else
rc << r.id
end
end
rc
end
def run_asynchronous(*args, &block)
options =
if args.last.is_a?(Hash)
args.pop
else
{}
end
input =
case args.length
when 0
options[:input] || []
when 1
args[0]
else
args
end
input = Cenit::Utility.json_value_of(input)
input = [input] unless input.is_a?(Array)
Setup::AlgorithmExecution.process(options.merge(algorithm_id: id.to_s, input: input), &block)
end
def run(input = [], task = nil)
input = Cenit::Utility.json_value_of(input)
input = [input] unless input.is_a?(Array)
input = input.dup
input_size = input.length
# Initialize the parameters not entered, with its default values
parameters[input_size..].to_a.each { |p| input << p.default }
# Initialize task parameter
if index = task_parameter_index
input_size += 1 if parameters[index].required && index >= input_size
input[index] = task || Setup::Task.current if input[index].nil?
end
# Check required parameters
required_size = required_parameters_size
fail "expected #{required_size} args but got #{input_size}" if input_size < required_size
begin
rc = Cenit::BundlerInterpreter.run(self, *input)
rescue Exception => ex
ex.backtrace.unshift("In algorithm #{namespace}::#{name}")
raise ex
end
if rc
if store_output
unless output_datatype
fail 'Execution failed! Output storage required and no Output DataType defined.'
end
begin
ids = do_store_output(rc)
args = {}
parameters.each { |parameter| args[parameter.name] = input.shift }
@last_output = AlgorithmOutput.create(
algorithm: self,
data_type: output_datatype,
input_params: args,
output_ids: ids)
rescue Exception => e
fail "Failing storing output: #{e.message}" if validate_output
end
end
end
rc
end
def task_parameter_index
# TODO Force task parameter type when parameters types include cenit data types
parameters.to_a.index { |p| p.name == 'task' }
end
def link?(call_symbol)
link(call_symbol).present?
end
def link(call_symbol)
if (call_link = call_links.where(name: call_symbol).first)
call_link.do_link
else
nil
end
end
def linker_id
id.to_s
end
def for_each_call(visited = Set.new, &block)
unless visited.include?(self)
visited << self
block.call(self) if block
call_links.each { |call_link| call_link.link.for_each_call(visited, &block) if call_link.link }
end
end
def stored_outputs(_options = {})
AlgorithmOutput.where(algorithm: self).desc(:created_at)
end
def configuration_schema
schema =
{
type: 'object',
properties: properties = {},
required: parameters
.select { |p| p.required && p.name != 'task' }
.collect(&:name)
}
parameters.each { |p| properties[p.name] = p.schema }
schema.stringify_keys
end
def configuration_model
@mongoff_model ||= Mongoff::Model.for(
data_type: self.class.data_type,
schema: configuration_schema,
name: self.class.configuration_model_name,
cache: false)
end
def language_name
self.class.language_enum.keys.detect { |key| self.class.language_enum[key] == language }
end
class << self
def language_enum
{
'Auto detect': :auto,
# 'Python': :python,
# 'PHP': :php,
'JavaScript': :javascript,
'Ruby': :ruby
}
end
def configuration_model_name
"#{Setup::Algorithm}::Config"
end
end
def parse_code
if language == :auto
logs = {}
lang = self.class.language_enum.values.detect do |language|
next if language == :auto
logs.clear
parse_method = "parse_#{language}_code"
logs.merge!(send(parse_method))
logs[:errors].blank?
end
if lang
self.language = lang
else
logs.clear
logs[:errors] = ["can't be auto-detected with syntax errors or typed language is not supported"]
end
logs
else
parse_method = "parse_#{language}_code"
send(parse_method)
end
end
protected
def parse_ruby_code
logs = {}
unless Capataz.rewrite(code, halt_on_error: false, logs: logs, locals: parameters.collect(&:name))
(logs[:errors] ||= []) << 'with no valid Ruby syntax'
end
logs
end
def parse_javascript_code
logs = { errors: errors = [] }
ast =
begin
RKelly.parse(code)
rescue Exception
nil
end
if ast
logs[:self_sends] = call_names = Set.new
ast.each do |node|
if node.is_a?(RKelly::Nodes::FunctionCallNode) && (node = node.value).is_a?(RKelly::Nodes::ResolveNode)
call_names << node.value
end
end
else
errors << 'with no valid JavaScript syntax'
end
logs
end
def parse_php_code
{
errors: ['PHP parsing not yet supported']
}
end
def parse_python_code
{
errors: ['Python parsing not yet supported']
}
end
end
end
class Array
def range=(arg)
@range = arg
end
end