braintrust 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/braintrust/api/functions.rb +11 -0
- data/lib/braintrust/internal/template.rb +91 -0
- data/lib/braintrust/prompt.rb +143 -0
- data/lib/braintrust/vendor/mustache/context.rb +180 -0
- data/lib/braintrust/vendor/mustache/context_miss.rb +22 -0
- data/lib/braintrust/vendor/mustache/enumerable.rb +14 -0
- data/lib/braintrust/vendor/mustache/generator.rb +188 -0
- data/lib/braintrust/vendor/mustache/mustache.rb +260 -0
- data/lib/braintrust/vendor/mustache/parser.rb +364 -0
- data/lib/braintrust/vendor/mustache/settings.rb +252 -0
- data/lib/braintrust/vendor/mustache/template.rb +138 -0
- data/lib/braintrust/vendor/mustache/utils.rb +42 -0
- data/lib/braintrust/vendor/mustache.rb +16 -0
- data/lib/braintrust/version.rb +1 -1
- data/lib/braintrust.rb +1 -0
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 626876b443795d28b4ba5d12f8bf10381c3052d5d196adb01207d545303f3d1e
|
|
4
|
+
data.tar.gz: 347ca89ea9f485ca6521a38c067bdd15074db4e6a4757523901888a8d4cc3e9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b827a4f92e2bc4b39e41174e62dacdc431fdc0b6c8d13882bdcaaa369af9621174fc01bafa3d0d594b71464ea374c6904e9834631957951dad87d6583a58dc9
|
|
7
|
+
data.tar.gz: bb6f2d3807765ef4ad591849e0972379fc3f97ef8d90bda0785e1d4dab87ce5e91d954d9d3c8fc7eff6c9295d120d6cbe07acb5bb348873c842d791a3fbdce84
|
|
@@ -85,6 +85,17 @@ module Braintrust
|
|
|
85
85
|
http_post_json("/v1/function/#{id}/invoke", payload)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Get a function by ID (includes full prompt_data)
|
|
89
|
+
# GET /v1/function/{id}
|
|
90
|
+
# @param id [String] Function UUID
|
|
91
|
+
# @param version [String, nil] Retrieve prompt at a specific version (transaction ID or version identifier)
|
|
92
|
+
# @return [Hash] Full function data including prompt_data
|
|
93
|
+
def get(id:, version: nil)
|
|
94
|
+
params = {}
|
|
95
|
+
params["version"] = version if version
|
|
96
|
+
http_get("/v1/function/#{id}", params)
|
|
97
|
+
end
|
|
98
|
+
|
|
88
99
|
# Delete a function by ID
|
|
89
100
|
# DELETE /v1/function/{id}
|
|
90
101
|
# @param id [String] Function UUID
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../vendor/mustache"
|
|
4
|
+
|
|
5
|
+
module Braintrust
|
|
6
|
+
module Internal
|
|
7
|
+
# Template rendering utilities for Mustache templates
|
|
8
|
+
module Template
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Render a template string with variable substitution
|
|
12
|
+
#
|
|
13
|
+
# @param text [String] Template text to render
|
|
14
|
+
# @param variables [Hash] Variables to substitute
|
|
15
|
+
# @param format [String] Template format: "mustache", "none", or "nunjucks"
|
|
16
|
+
# @param strict [Boolean] Raise error on missing variables (default: false)
|
|
17
|
+
# @return [String] Rendered text
|
|
18
|
+
def render(text, variables, format:, strict: false)
|
|
19
|
+
return text unless text.is_a?(String)
|
|
20
|
+
|
|
21
|
+
case format
|
|
22
|
+
when "none"
|
|
23
|
+
# No templating - return text unchanged
|
|
24
|
+
text
|
|
25
|
+
when "nunjucks"
|
|
26
|
+
# Nunjucks is a UI-only feature in Braintrust
|
|
27
|
+
raise Error, "Nunjucks templates are not supported in the Ruby SDK. " \
|
|
28
|
+
"Nunjucks only works in Braintrust playgrounds. " \
|
|
29
|
+
"Please use 'mustache' or 'none' template format, or invoke the prompt via the API proxy."
|
|
30
|
+
when "mustache", "", nil
|
|
31
|
+
# Default: Mustache templating
|
|
32
|
+
if strict
|
|
33
|
+
missing = find_missing_variables(text, variables)
|
|
34
|
+
if missing.any?
|
|
35
|
+
raise Error, "Missing required variables: #{missing.join(", ")}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
Vendor::Mustache.render(text, variables)
|
|
40
|
+
else
|
|
41
|
+
raise Error, "Unknown template format: #{format.inspect}. " \
|
|
42
|
+
"Supported formats are 'mustache' and 'none'."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Find Mustache variables in template that are not provided
|
|
47
|
+
#
|
|
48
|
+
# @param text [String] Template text
|
|
49
|
+
# @param variables [Hash] Available variables
|
|
50
|
+
# @return [Array<String>] List of missing variable names
|
|
51
|
+
def find_missing_variables(text, variables)
|
|
52
|
+
# Extract {{variable}} and {{variable.path}} patterns
|
|
53
|
+
# Mustache uses {{name}} syntax
|
|
54
|
+
text.scan(/\{\{([^}#^\/!>]+)\}\}/).flatten.map(&:strip).uniq.reject do |var|
|
|
55
|
+
resolve_variable(var, variables)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if a variable path exists in a hash
|
|
60
|
+
#
|
|
61
|
+
# @param path [String] Dot-separated variable path (e.g., "user.name")
|
|
62
|
+
# @param variables [Hash] Variables to search
|
|
63
|
+
# @return [Object, nil] The value if found, nil otherwise
|
|
64
|
+
def resolve_variable(path, variables)
|
|
65
|
+
parts = path.split(".")
|
|
66
|
+
value = variables
|
|
67
|
+
|
|
68
|
+
parts.each do |part|
|
|
69
|
+
return nil unless value.is_a?(Hash)
|
|
70
|
+
# Try both string and symbol keys
|
|
71
|
+
value = value[part] || value[part.to_sym]
|
|
72
|
+
return nil if value.nil?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Convert hash keys to strings recursively
|
|
79
|
+
#
|
|
80
|
+
# @param hash [Hash] Hash with symbol or string keys
|
|
81
|
+
# @return [Hash] Hash with all string keys
|
|
82
|
+
def stringify_keys(hash)
|
|
83
|
+
return {} unless hash.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
hash.transform_keys(&:to_s).transform_values do |v|
|
|
86
|
+
v.is_a?(Hash) ? stringify_keys(v) : v
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "internal/template"
|
|
5
|
+
|
|
6
|
+
module Braintrust
|
|
7
|
+
# Prompt class for loading and building prompts from Braintrust
|
|
8
|
+
#
|
|
9
|
+
# @example Load and use a prompt
|
|
10
|
+
# prompt = Braintrust::Prompt.load(project: "my-project", slug: "summarizer")
|
|
11
|
+
# params = prompt.build(text: "Article to summarize...")
|
|
12
|
+
# client.messages.create(**params)
|
|
13
|
+
class Prompt
|
|
14
|
+
attr_reader :id, :name, :slug, :project_id
|
|
15
|
+
|
|
16
|
+
# Load a prompt from Braintrust
|
|
17
|
+
#
|
|
18
|
+
# @param project [String] Project name
|
|
19
|
+
# @param slug [String] Prompt slug
|
|
20
|
+
# @param version [String, nil] Specific version (default: latest)
|
|
21
|
+
# @param defaults [Hash] Default variable values for build()
|
|
22
|
+
# @param api [API, nil] Braintrust API client (default: creates one using global state)
|
|
23
|
+
# @return [Prompt]
|
|
24
|
+
def self.load(project:, slug:, version: nil, defaults: {}, api: nil)
|
|
25
|
+
api ||= API.new
|
|
26
|
+
|
|
27
|
+
# Find the function by project + slug
|
|
28
|
+
result = api.functions.list(project_name: project, slug: slug)
|
|
29
|
+
function = result.dig("objects")&.first
|
|
30
|
+
raise Error, "Prompt '#{slug}' not found in project '#{project}'" unless function
|
|
31
|
+
|
|
32
|
+
# Fetch full function data including prompt_data
|
|
33
|
+
full_data = api.functions.get(id: function["id"], version: version)
|
|
34
|
+
|
|
35
|
+
new(full_data, defaults: defaults)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Initialize a Prompt from function data
|
|
39
|
+
#
|
|
40
|
+
# @param data [Hash] Function data from API
|
|
41
|
+
# @param defaults [Hash] Default variable values for build()
|
|
42
|
+
def initialize(data, defaults: {})
|
|
43
|
+
@data = data
|
|
44
|
+
@defaults = Internal::Template.stringify_keys(defaults)
|
|
45
|
+
|
|
46
|
+
@id = data["id"]
|
|
47
|
+
@name = data["name"]
|
|
48
|
+
@slug = data["slug"]
|
|
49
|
+
@project_id = data["project_id"]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Get the raw prompt definition
|
|
53
|
+
# @return [Hash, nil]
|
|
54
|
+
def prompt
|
|
55
|
+
@data.dig("prompt_data", "prompt")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get the prompt messages
|
|
59
|
+
# @return [Array<Hash>]
|
|
60
|
+
def messages
|
|
61
|
+
prompt&.dig("messages") || []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the tools definition (parsed from JSON string)
|
|
65
|
+
# @return [Array<Hash>, nil]
|
|
66
|
+
def tools
|
|
67
|
+
tools_json = prompt&.dig("tools")
|
|
68
|
+
return nil unless tools_json.is_a?(String) && !tools_json.empty?
|
|
69
|
+
|
|
70
|
+
JSON.parse(tools_json)
|
|
71
|
+
rescue JSON::ParserError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get the model name
|
|
76
|
+
# @return [String, nil]
|
|
77
|
+
def model
|
|
78
|
+
@data.dig("prompt_data", "options", "model")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Get model options
|
|
82
|
+
# @return [Hash]
|
|
83
|
+
def options
|
|
84
|
+
@data.dig("prompt_data", "options") || {}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get the template format
|
|
88
|
+
# @return [String] "mustache" (default), "nunjucks", or "none"
|
|
89
|
+
def template_format
|
|
90
|
+
@data.dig("prompt_data", "template_format") || "mustache"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Build the prompt with variable substitution
|
|
94
|
+
#
|
|
95
|
+
# Returns a hash ready to pass to an LLM client:
|
|
96
|
+
# {model: "...", messages: [...], temperature: ..., ...}
|
|
97
|
+
#
|
|
98
|
+
# @param variables [Hash] Variables to substitute (e.g., {name: "Alice"})
|
|
99
|
+
# @param strict [Boolean] Raise error on missing variables (default: false)
|
|
100
|
+
# @return [Hash] Built prompt ready for LLM client
|
|
101
|
+
#
|
|
102
|
+
# @example With keyword arguments
|
|
103
|
+
# prompt.build(name: "Alice", task: "coding")
|
|
104
|
+
#
|
|
105
|
+
# @example With explicit hash
|
|
106
|
+
# prompt.build({name: "Alice"}, strict: true)
|
|
107
|
+
def build(variables = nil, strict: false, **kwargs)
|
|
108
|
+
# Support both explicit hash and keyword arguments
|
|
109
|
+
variables_hash = variables.is_a?(Hash) ? variables : {}
|
|
110
|
+
vars = @defaults
|
|
111
|
+
.merge(Internal::Template.stringify_keys(variables_hash))
|
|
112
|
+
.merge(Internal::Template.stringify_keys(kwargs))
|
|
113
|
+
|
|
114
|
+
# Render Mustache templates in messages
|
|
115
|
+
built_messages = messages.map do |msg|
|
|
116
|
+
{
|
|
117
|
+
role: msg["role"].to_sym,
|
|
118
|
+
content: Internal::Template.render(msg["content"], vars, format: template_format, strict: strict)
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Build result with model and messages
|
|
123
|
+
result = {
|
|
124
|
+
model: model,
|
|
125
|
+
messages: built_messages
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Add tools if defined
|
|
129
|
+
parsed_tools = tools
|
|
130
|
+
result[:tools] = parsed_tools if parsed_tools
|
|
131
|
+
|
|
132
|
+
# Add params (temperature, max_tokens, etc.) to top level
|
|
133
|
+
params = options.dig("params")
|
|
134
|
+
if params.is_a?(Hash)
|
|
135
|
+
params.each do |key, value|
|
|
136
|
+
result[key.to_sym] = value
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
result
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored from mustache gem v1.1.1
|
|
4
|
+
# https://github.com/mustache/mustache
|
|
5
|
+
# License: MIT
|
|
6
|
+
# Modifications: Namespaced under Braintrust::Vendor
|
|
7
|
+
|
|
8
|
+
require_relative "context_miss"
|
|
9
|
+
|
|
10
|
+
module Braintrust
|
|
11
|
+
module Vendor
|
|
12
|
+
class Mustache
|
|
13
|
+
# A Context represents the context which a Mustache template is
|
|
14
|
+
# executed within. All Mustache tags reference keys in the Context.
|
|
15
|
+
class Context
|
|
16
|
+
# Initializes a Mustache::Context.
|
|
17
|
+
#
|
|
18
|
+
# @param [Mustache] mustache A Mustache instance.
|
|
19
|
+
#
|
|
20
|
+
def initialize(mustache)
|
|
21
|
+
@stack = [mustache]
|
|
22
|
+
@partial_template_cache = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# A {{>partial}} tag translates into a call to the context's
|
|
26
|
+
# `partial` method, which would be this sucker right here.
|
|
27
|
+
#
|
|
28
|
+
# If the Mustache view handling the rendering (e.g. the view
|
|
29
|
+
# representing your profile page or some other template) responds
|
|
30
|
+
# to `partial`, we call it and render the result.
|
|
31
|
+
#
|
|
32
|
+
def partial(name, indentation = "")
|
|
33
|
+
# Look for the first Mustache in the stack.
|
|
34
|
+
mustache = mustache_in_stack
|
|
35
|
+
|
|
36
|
+
# Indent the partial template by the given indentation.
|
|
37
|
+
part = mustache.partial(name).to_s.gsub(/^/, indentation)
|
|
38
|
+
|
|
39
|
+
# Get a template object for the partial and render the result.
|
|
40
|
+
template_for_partial(part).render(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def template_for_partial(partial)
|
|
44
|
+
@partial_template_cache[partial] ||= Template.new(partial)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Find the first Mustache in the stack.
|
|
48
|
+
#
|
|
49
|
+
# If we're being rendered inside a Mustache object as a context,
|
|
50
|
+
# we'll use that one.
|
|
51
|
+
#
|
|
52
|
+
# @return [Mustache] First Mustache in the stack.
|
|
53
|
+
#
|
|
54
|
+
def mustache_in_stack
|
|
55
|
+
@mustache_in_stack ||= @stack.find { |frame| frame.is_a?(Mustache) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Allows customization of how Mustache escapes things.
|
|
59
|
+
#
|
|
60
|
+
# @param [Object] value Value to escape.
|
|
61
|
+
#
|
|
62
|
+
# @return [String] Escaped string.
|
|
63
|
+
#
|
|
64
|
+
def escape(value)
|
|
65
|
+
mustache_in_stack.escape(value)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Adds a new object to the context's internal stack.
|
|
69
|
+
#
|
|
70
|
+
# @param [Object] new_obj Object to be added to the internal stack.
|
|
71
|
+
#
|
|
72
|
+
# @return [Context] Returns the Context.
|
|
73
|
+
#
|
|
74
|
+
def push(new_obj)
|
|
75
|
+
@stack.unshift(new_obj)
|
|
76
|
+
@mustache_in_stack = nil
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Removes the most recently added object from the context's
|
|
81
|
+
# internal stack.
|
|
82
|
+
#
|
|
83
|
+
# @return [Context] Returns the Context.
|
|
84
|
+
#
|
|
85
|
+
def pop
|
|
86
|
+
@stack.shift
|
|
87
|
+
@mustache_in_stack = nil
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Can be used to add a value to the context in a hash-like way.
|
|
92
|
+
#
|
|
93
|
+
# context[:name] = "Chris"
|
|
94
|
+
def []=(name, value)
|
|
95
|
+
push(name => value)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Alias for `fetch`.
|
|
99
|
+
def [](name)
|
|
100
|
+
fetch(name, nil)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Do we know about a particular key? In other words, will calling
|
|
104
|
+
# `context[key]` give us a result that was set. Basically.
|
|
105
|
+
def has_key?(key)
|
|
106
|
+
fetch(key, false)
|
|
107
|
+
rescue ContextMiss
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Similar to Hash#fetch, finds a value by `name` in the context's
|
|
112
|
+
# stack. You may specify the default return value by passing a
|
|
113
|
+
# second parameter.
|
|
114
|
+
#
|
|
115
|
+
# If no second parameter is passed (or raise_on_context_miss is
|
|
116
|
+
# set to true), will raise a ContextMiss exception on miss.
|
|
117
|
+
def fetch(name, default = :__raise)
|
|
118
|
+
@stack.each do |frame|
|
|
119
|
+
# Prevent infinite recursion.
|
|
120
|
+
next if frame == self
|
|
121
|
+
|
|
122
|
+
value = find(frame, name, :__missing)
|
|
123
|
+
return value if :__missing != value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if default == :__raise || mustache_in_stack.raise_on_context_miss?
|
|
127
|
+
raise ContextMiss.new("Can't find #{name} in #{@stack.inspect}")
|
|
128
|
+
else
|
|
129
|
+
default
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Finds a key in an object, using whatever method is most
|
|
134
|
+
# appropriate. If the object is a hash, does a simple hash lookup.
|
|
135
|
+
# If it's an object that responds to the key as a method call,
|
|
136
|
+
# invokes that method. You get the idea.
|
|
137
|
+
#
|
|
138
|
+
# @param [Object] obj The object to perform the lookup on.
|
|
139
|
+
# @param [String,Symbol] key The key whose value you want
|
|
140
|
+
# @param [Object] default An optional default value, to return if the key is not found.
|
|
141
|
+
#
|
|
142
|
+
# @return [Object] The value of key in object if it is found, and default otherwise.
|
|
143
|
+
#
|
|
144
|
+
def find(obj, key, default = nil)
|
|
145
|
+
return find_in_hash(obj.to_hash, key, default) if obj.respond_to?(:to_hash)
|
|
146
|
+
|
|
147
|
+
unless obj.respond_to?(key)
|
|
148
|
+
# no match for the key, but it may include a hyphen, so try again replacing hyphens with underscores.
|
|
149
|
+
key = key.to_s.tr("-", "_")
|
|
150
|
+
return default unless obj.respond_to?(key)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
meth = obj.method(key) rescue proc { obj.send(key) }
|
|
154
|
+
meth.arity == 1 ? meth.to_proc : meth.call
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def current
|
|
158
|
+
@stack.first
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
# Fetches a hash key if it exists, or returns the given default.
|
|
164
|
+
def find_in_hash(obj, key, default)
|
|
165
|
+
return obj[key] if obj.has_key?(key)
|
|
166
|
+
return obj[key.to_s] if obj.has_key?(key.to_s)
|
|
167
|
+
return obj[key] if obj.respond_to?(:default_proc) && obj.default_proc && obj[key]
|
|
168
|
+
|
|
169
|
+
# If default is :__missing then we are from #fetch which is hunting through the stack
|
|
170
|
+
# If default is nil then we are reducing dot notation
|
|
171
|
+
if :__missing != default && mustache_in_stack.raise_on_context_miss?
|
|
172
|
+
raise ContextMiss.new("Can't find #{key} in #{obj}")
|
|
173
|
+
else
|
|
174
|
+
obj.fetch(key, default)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored from mustache gem v1.1.1
|
|
4
|
+
# https://github.com/mustache/mustache
|
|
5
|
+
# License: MIT
|
|
6
|
+
# Modifications: Namespaced under Braintrust::Vendor
|
|
7
|
+
|
|
8
|
+
module Braintrust
|
|
9
|
+
module Vendor
|
|
10
|
+
class Mustache
|
|
11
|
+
# A ContextMiss is raised whenever a tag's target can not be found
|
|
12
|
+
# in the current context if `Mustache#raise_on_context_miss?` is
|
|
13
|
+
# set to true.
|
|
14
|
+
#
|
|
15
|
+
# For example, if your View class does not respond to `music` but
|
|
16
|
+
# your template contains a `{{music}}` tag this exception will be raised.
|
|
17
|
+
#
|
|
18
|
+
# By default it is not raised. See Mustache.raise_on_context_miss.
|
|
19
|
+
class ContextMiss < RuntimeError; end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored from mustache gem v1.1.1
|
|
4
|
+
# https://github.com/mustache/mustache
|
|
5
|
+
# License: MIT
|
|
6
|
+
# Modifications: Namespaced under Braintrust::Vendor
|
|
7
|
+
|
|
8
|
+
module Braintrust
|
|
9
|
+
module Vendor
|
|
10
|
+
class Mustache
|
|
11
|
+
Enumerable = Module.new
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Vendored from mustache gem v1.1.1
|
|
4
|
+
# https://github.com/mustache/mustache
|
|
5
|
+
# License: MIT
|
|
6
|
+
# Modifications: Namespaced under Braintrust::Vendor
|
|
7
|
+
|
|
8
|
+
module Braintrust
|
|
9
|
+
module Vendor
|
|
10
|
+
class Mustache
|
|
11
|
+
# The Generator is in charge of taking an array of Mustache tokens,
|
|
12
|
+
# usually assembled by the Parser, and generating an interpolatable
|
|
13
|
+
# Ruby string. This string is considered the "compiled" template
|
|
14
|
+
# because at that point we're relying on Ruby to do the parsing and
|
|
15
|
+
# run our code.
|
|
16
|
+
#
|
|
17
|
+
# For example, let's take this template:
|
|
18
|
+
#
|
|
19
|
+
# Hi {{thing}}!
|
|
20
|
+
#
|
|
21
|
+
# If we run this through the Parser we'll get these tokens:
|
|
22
|
+
#
|
|
23
|
+
# [:multi,
|
|
24
|
+
# [:static, "Hi "],
|
|
25
|
+
# [:mustache, :etag, "thing"],
|
|
26
|
+
# [:static, "!\n"]]
|
|
27
|
+
#
|
|
28
|
+
# Now let's hand that to the Generator:
|
|
29
|
+
#
|
|
30
|
+
# >> puts Braintrust::Vendor::Mustache::Generator.new.compile(tokens)
|
|
31
|
+
# "Hi #{ctx.escape(ctx[:thing])}!\n"
|
|
32
|
+
class Generator
|
|
33
|
+
# Options can be used to manipulate the resulting ruby code string behavior.
|
|
34
|
+
def initialize(options = {})
|
|
35
|
+
@options = options
|
|
36
|
+
@option_static_lambdas = options[:static_lambdas] == true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Given an array of tokens, returns an interpolatable Ruby string.
|
|
40
|
+
def compile(exp)
|
|
41
|
+
"\"#{compile!(exp)}\""
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Given an array of tokens, converts them into Ruby code. In
|
|
47
|
+
# particular there are three types of expressions we are concerned
|
|
48
|
+
# with:
|
|
49
|
+
#
|
|
50
|
+
# :multi
|
|
51
|
+
# Mixed bag of :static, :mustache, and whatever.
|
|
52
|
+
#
|
|
53
|
+
# :static
|
|
54
|
+
# Normal HTML, the stuff outside of {{mustaches}}.
|
|
55
|
+
#
|
|
56
|
+
# :mustache
|
|
57
|
+
# Any Mustache tag, from sections to partials.
|
|
58
|
+
def compile!(exp)
|
|
59
|
+
case exp.first
|
|
60
|
+
when :multi
|
|
61
|
+
exp[1..-1].reduce(+"") { |sum, e| sum << compile!(e) }
|
|
62
|
+
when :static
|
|
63
|
+
str(exp[1])
|
|
64
|
+
when :mustache
|
|
65
|
+
send(:"on_#{exp[1]}", *exp[2..-1])
|
|
66
|
+
else
|
|
67
|
+
raise "Unhandled exp: #{exp.first}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Callback fired when the compiler finds a section token. We're
|
|
72
|
+
# passed the section name and the array of tokens.
|
|
73
|
+
def on_section(name, offset, content, raw, delims)
|
|
74
|
+
# Convert the tokenized content of this section into a Ruby
|
|
75
|
+
# string we can use.
|
|
76
|
+
code = compile(content)
|
|
77
|
+
|
|
78
|
+
# Lambda handling - default handling is to dynamically interpret
|
|
79
|
+
# the returned lambda result as mustache source
|
|
80
|
+
proc_handling = if @option_static_lambdas
|
|
81
|
+
<<-compiled
|
|
82
|
+
v.call(lambda {|v| #{code}}.call(v)).to_s
|
|
83
|
+
compiled
|
|
84
|
+
else
|
|
85
|
+
<<-compiled
|
|
86
|
+
t = Braintrust::Vendor::Mustache::Template.new(v.call(#{raw.inspect}).to_s)
|
|
87
|
+
def t.tokens(src=@source)
|
|
88
|
+
p = Braintrust::Vendor::Mustache::Parser.new
|
|
89
|
+
p.otag, p.ctag = #{delims.inspect}
|
|
90
|
+
p.compile(src)
|
|
91
|
+
end
|
|
92
|
+
t.render(ctx.dup)
|
|
93
|
+
compiled
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Compile the Ruby for this section now that we know what's
|
|
97
|
+
# inside the section.
|
|
98
|
+
ev(<<-compiled)
|
|
99
|
+
case v = #{compile!(name)}
|
|
100
|
+
when NilClass, FalseClass
|
|
101
|
+
when TrueClass
|
|
102
|
+
#{code}
|
|
103
|
+
when Proc
|
|
104
|
+
#{proc_handling}
|
|
105
|
+
when Array, Enumerator, Braintrust::Vendor::Mustache::Enumerable
|
|
106
|
+
v.map { |_| ctx.push(_); r = #{code}; ctx.pop; r }.join
|
|
107
|
+
else
|
|
108
|
+
ctx.push(v); r = #{code}; ctx.pop; r
|
|
109
|
+
end
|
|
110
|
+
compiled
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Fired when we find an inverted section. Just like `on_section`,
|
|
114
|
+
# we're passed the inverted section name and the array of tokens.
|
|
115
|
+
def on_inverted_section(name, offset, content, raw, delims)
|
|
116
|
+
# Convert the tokenized content of this section into a Ruby
|
|
117
|
+
# string we can use.
|
|
118
|
+
code = compile(content)
|
|
119
|
+
|
|
120
|
+
# Compile the Ruby for this inverted section now that we know
|
|
121
|
+
# what's inside.
|
|
122
|
+
ev(<<-compiled)
|
|
123
|
+
v = #{compile!(name)}
|
|
124
|
+
if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
|
|
125
|
+
#{code}
|
|
126
|
+
end
|
|
127
|
+
compiled
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Fired when the compiler finds a partial. We want to return code
|
|
131
|
+
# which calls a partial at runtime instead of expanding and
|
|
132
|
+
# including the partial's body to allow for recursive partials.
|
|
133
|
+
def on_partial(name, offset, indentation)
|
|
134
|
+
ev("ctx.partial(#{name.to_sym.inspect}, #{indentation.inspect})")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# An unescaped tag.
|
|
138
|
+
def on_utag(name, offset)
|
|
139
|
+
ev(<<-compiled)
|
|
140
|
+
v = #{compile!(name)}
|
|
141
|
+
if v.is_a?(Proc)
|
|
142
|
+
v = #{@option_static_lambdas ? "v.call" : "Braintrust::Vendor::Mustache::Template.new(v.call.to_s).render(ctx.dup)"}
|
|
143
|
+
end
|
|
144
|
+
v.to_s
|
|
145
|
+
compiled
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# An escaped tag.
|
|
149
|
+
def on_etag(name, offset)
|
|
150
|
+
ev(<<-compiled)
|
|
151
|
+
v = #{compile!(name)}
|
|
152
|
+
if v.is_a?(Proc)
|
|
153
|
+
v = #{@option_static_lambdas ? "v.call" : "Braintrust::Vendor::Mustache::Template.new(v.call.to_s).render(ctx.dup)"}
|
|
154
|
+
end
|
|
155
|
+
ctx.escape(v)
|
|
156
|
+
compiled
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def on_fetch(names)
|
|
160
|
+
return "ctx.current" if names.empty?
|
|
161
|
+
|
|
162
|
+
names = names.map { |n| n.to_sym }
|
|
163
|
+
|
|
164
|
+
initial, *rest = names
|
|
165
|
+
if rest.any?
|
|
166
|
+
<<-compiled
|
|
167
|
+
#{rest.inspect}.reduce(ctx[#{initial.inspect}]) { |value, key| value && ctx.find(value, key) }
|
|
168
|
+
compiled
|
|
169
|
+
else
|
|
170
|
+
<<-compiled
|
|
171
|
+
ctx[#{initial.inspect}]
|
|
172
|
+
compiled
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# An interpolation-friendly version of a string, for use within a
|
|
177
|
+
# Ruby string.
|
|
178
|
+
def ev(s)
|
|
179
|
+
"#\{#{s}}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def str(s)
|
|
183
|
+
s.inspect[1..-2]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|