prompt_manager 0.5.7 → 0.5.8
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/CHANGELOG.md +4 -0
- data/COMMITS.md +196 -0
- data/README.md +485 -203
- data/docs/.keep +0 -0
- data/docs/advanced/custom-keywords.md +421 -0
- data/docs/advanced/dynamic-directives.md +535 -0
- data/docs/advanced/performance.md +612 -0
- data/docs/advanced/search-integration.md +635 -0
- data/docs/api/configuration.md +355 -0
- data/docs/api/directive-processor.md +431 -0
- data/docs/api/prompt-class.md +354 -0
- data/docs/api/storage-adapters.md +462 -0
- data/docs/assets/favicon.ico +1 -0
- data/docs/assets/logo.svg +24 -0
- data/docs/core-features/comments.md +48 -0
- data/docs/core-features/directive-processing.md +38 -0
- data/docs/core-features/erb-integration.md +68 -0
- data/docs/core-features/error-handling.md +197 -0
- data/docs/core-features/parameter-history.md +76 -0
- data/docs/core-features/parameterized-prompts.md +500 -0
- data/docs/core-features/shell-integration.md +79 -0
- data/docs/development/architecture.md +544 -0
- data/docs/development/contributing.md +425 -0
- data/docs/development/roadmap.md +234 -0
- data/docs/development/testing.md +822 -0
- data/docs/examples/advanced.md +523 -0
- data/docs/examples/basic.md +688 -0
- data/docs/examples/real-world.md +776 -0
- data/docs/examples.md +337 -0
- data/docs/getting-started/basic-concepts.md +318 -0
- data/docs/getting-started/installation.md +97 -0
- data/docs/getting-started/quick-start.md +256 -0
- data/docs/index.md +230 -0
- data/docs/migration/v0.9.0.md +459 -0
- data/docs/migration/v1.0.0.md +591 -0
- data/docs/storage/activerecord-adapter.md +348 -0
- data/docs/storage/custom-adapters.md +176 -0
- data/docs/storage/filesystem-adapter.md +236 -0
- data/docs/storage/overview.md +427 -0
- data/examples/advanced_integrations.rb +52 -0
- data/examples/prompts_dir/advanced_demo.txt +79 -0
- data/examples/prompts_dir/directive_example.json +1 -0
- data/examples/prompts_dir/directive_example.txt +8 -0
- data/examples/prompts_dir/todo.json +1 -1
- data/improvement_plan.md +996 -0
- data/lib/prompt_manager/storage/file_system_adapter.rb +8 -2
- data/lib/prompt_manager/version.rb +1 -1
- data/mkdocs.yml +146 -0
- data/prompt_manager_logo.png +0 -0
- metadata +46 -3
- data/LICENSE.txt +0 -21
@@ -0,0 +1,535 @@
|
|
1
|
+
# Dynamic Directives
|
2
|
+
|
3
|
+
Dynamic directives allow you to create sophisticated, runtime-configurable prompt processing behaviors that go beyond static `//include` statements.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Dynamic directives are custom processing instructions that can modify prompt content based on runtime conditions, external data sources, user context, and complex business logic.
|
8
|
+
|
9
|
+
## Creating Dynamic Directives
|
10
|
+
|
11
|
+
### Basic Dynamic Directive
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
PromptManager.configure do |config|
|
15
|
+
config.directive_processor.register_directive('current_time') do |args, context|
|
16
|
+
format = args.strip.empty? ? '%Y-%m-%d %H:%M:%S' : args.strip
|
17
|
+
Time.current.strftime(format)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
```text
|
23
|
+
# Usage in prompts:
|
24
|
+
Generated at: //current_time
|
25
|
+
Custom format: //current_time %B %d, %Y at %I:%M %p
|
26
|
+
```
|
27
|
+
|
28
|
+
### Context-Aware Directives
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
config.directive_processor.register_directive('user_greeting') do |args, context|
|
32
|
+
user = context.dig(:parameters, :user)
|
33
|
+
return 'Hello there!' unless user
|
34
|
+
|
35
|
+
time_of_day = Time.current.hour
|
36
|
+
greeting = case time_of_day
|
37
|
+
when 0..11 then 'Good morning'
|
38
|
+
when 12..17 then 'Good afternoon'
|
39
|
+
else 'Good evening'
|
40
|
+
end
|
41
|
+
|
42
|
+
"#{greeting}, #{user[:name]}!"
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
## Advanced Directive Patterns
|
47
|
+
|
48
|
+
### Data Loading Directives
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
config.directive_processor.register_directive('load_user_data') do |user_id, context|
|
52
|
+
begin
|
53
|
+
user = UserService.find(user_id)
|
54
|
+
context[:loaded_user] = user
|
55
|
+
|
56
|
+
# Return user summary
|
57
|
+
<<~USER_INFO
|
58
|
+
User: #{user.name}
|
59
|
+
Email: #{user.email}
|
60
|
+
Account Type: #{user.account_type}
|
61
|
+
USER_INFO
|
62
|
+
rescue UserNotFoundError
|
63
|
+
'User information unavailable'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
### API Integration Directives
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
config.directive_processor.register_directive('weather') do |location, context|
|
72
|
+
begin
|
73
|
+
response = HTTParty.get(
|
74
|
+
'https://api.weather.example.com/current',
|
75
|
+
query: {
|
76
|
+
location: location,
|
77
|
+
api_key: ENV['WEATHER_API_KEY']
|
78
|
+
}
|
79
|
+
)
|
80
|
+
|
81
|
+
if response.success?
|
82
|
+
data = response.parsed_response
|
83
|
+
"Weather in #{location}: #{data['temperature']}°F, #{data['condition']}"
|
84
|
+
else
|
85
|
+
'Weather information unavailable'
|
86
|
+
end
|
87
|
+
rescue => e
|
88
|
+
Rails.logger.error "Weather API error: #{e.message}"
|
89
|
+
'Weather service temporarily unavailable'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
### Database Query Directives
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
config.directive_processor.register_directive('recent_orders') do |user_id, context|
|
98
|
+
user = User.find(user_id)
|
99
|
+
recent_orders = user.orders.recent.limit(5)
|
100
|
+
|
101
|
+
if recent_orders.any?
|
102
|
+
orders_list = recent_orders.map do |order|
|
103
|
+
"- Order ##{order.id}: #{order.total_formatted} (#{order.status})"
|
104
|
+
end.join("\n")
|
105
|
+
|
106
|
+
"Your recent orders:\n#{orders_list}"
|
107
|
+
else
|
108
|
+
'You have no recent orders.'
|
109
|
+
end
|
110
|
+
rescue ActiveRecord::RecordNotFound
|
111
|
+
'User not found'
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Conditional Logic Directives
|
116
|
+
|
117
|
+
### IF/ELSE/ENDIF Directive System
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
class ConditionalDirectiveProcessor
|
121
|
+
def self.register_conditionals(processor)
|
122
|
+
processor.register_directive('if') do |condition, context|
|
123
|
+
context[:if_stack] ||= []
|
124
|
+
|
125
|
+
result = evaluate_condition(condition, context)
|
126
|
+
context[:if_stack].push({
|
127
|
+
condition_met: result,
|
128
|
+
in_else: false,
|
129
|
+
content: ''
|
130
|
+
})
|
131
|
+
|
132
|
+
'' # Don't output anything for the if directive itself
|
133
|
+
end
|
134
|
+
|
135
|
+
processor.register_directive('else') do |args, context|
|
136
|
+
return '' unless context[:if_stack]&.any?
|
137
|
+
|
138
|
+
current_if = context[:if_stack].last
|
139
|
+
current_if[:in_else] = true
|
140
|
+
|
141
|
+
'' # Don't output anything for the else directive
|
142
|
+
end
|
143
|
+
|
144
|
+
processor.register_directive('endif') do |args, context|
|
145
|
+
return '' unless context[:if_stack]&.any?
|
146
|
+
|
147
|
+
if_block = context[:if_stack].pop
|
148
|
+
|
149
|
+
# Process the accumulated content based on conditions
|
150
|
+
if (if_block[:condition_met] && !if_block[:in_else]) ||
|
151
|
+
(!if_block[:condition_met] && if_block[:in_else])
|
152
|
+
processor.process(if_block[:content], context)
|
153
|
+
else
|
154
|
+
''
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.evaluate_condition(condition, context)
|
160
|
+
# Simple condition evaluation
|
161
|
+
# In production, use a proper expression evaluator
|
162
|
+
case condition.strip
|
163
|
+
when /\[(\w+)\]\s*(==|!=|>|<|>=|<=)\s*(.+)/
|
164
|
+
param_name = Regexp.last_match(1).downcase.to_sym
|
165
|
+
operator = Regexp.last_match(2)
|
166
|
+
expected_value = Regexp.last_match(3).strip.gsub(/['"]/, '')
|
167
|
+
|
168
|
+
actual_value = context.dig(:parameters, param_name).to_s
|
169
|
+
|
170
|
+
case operator
|
171
|
+
when '==' then actual_value == expected_value
|
172
|
+
when '!=' then actual_value != expected_value
|
173
|
+
when '>' then actual_value.to_f > expected_value.to_f
|
174
|
+
when '<' then actual_value.to_f < expected_value.to_f
|
175
|
+
when '>=' then actual_value.to_f >= expected_value.to_f
|
176
|
+
when '<=' then actual_value.to_f <= expected_value.to_f
|
177
|
+
end
|
178
|
+
else
|
179
|
+
false
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Register the conditional directives
|
185
|
+
ConditionalDirectiveProcessor.register_conditionals(
|
186
|
+
PromptManager.configuration.directive_processor
|
187
|
+
)
|
188
|
+
```
|
189
|
+
|
190
|
+
```text
|
191
|
+
# Usage in prompts:
|
192
|
+
//if [USER_TYPE] == 'premium'
|
193
|
+
🌟 Welcome to Premium features!
|
194
|
+
//else
|
195
|
+
Upgrade to Premium for additional benefits.
|
196
|
+
//endif
|
197
|
+
|
198
|
+
//if [ORDER_TOTAL] >= 100
|
199
|
+
🚚 Free shipping applied!
|
200
|
+
//endif
|
201
|
+
```
|
202
|
+
|
203
|
+
### Loop Directives
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
config.directive_processor.register_directive('foreach') do |args, context|
|
207
|
+
array_name, template = args.split(':', 2)
|
208
|
+
array_data = context.dig(:parameters, array_name.strip.to_sym) || []
|
209
|
+
|
210
|
+
array_data.map.with_index do |item, index|
|
211
|
+
# Create item context
|
212
|
+
item_context = context.deep_dup
|
213
|
+
item_context[:parameters].merge!({
|
214
|
+
item: item,
|
215
|
+
item_index: index,
|
216
|
+
is_first: index == 0,
|
217
|
+
is_last: index == array_data.length - 1,
|
218
|
+
total_count: array_data.length
|
219
|
+
})
|
220
|
+
|
221
|
+
# Process template with item context
|
222
|
+
processed_template = template.gsub(/\[ITEM\.(\w+)\]/i) do |match|
|
223
|
+
property = Regexp.last_match(1).downcase
|
224
|
+
item.is_a?(Hash) ? item[property.to_sym] : item.send(property)
|
225
|
+
end
|
226
|
+
|
227
|
+
PromptManager::DirectiveProcessor.new.process(processed_template, item_context)
|
228
|
+
end.join("\n")
|
229
|
+
end
|
230
|
+
```
|
231
|
+
|
232
|
+
```text
|
233
|
+
# Usage in prompts:
|
234
|
+
Your order items:
|
235
|
+
//foreach order_items: - [ITEM.name]: $[ITEM.price] (Qty: [ITEM.quantity])
|
236
|
+
|
237
|
+
//foreach products:
|
238
|
+
**[ITEM.name]** - $[ITEM.price]
|
239
|
+
//if [ITEM.on_sale] == 'true'
|
240
|
+
🏷️ ON SALE!
|
241
|
+
//endif
|
242
|
+
```
|
243
|
+
|
244
|
+
## Template System Directives
|
245
|
+
|
246
|
+
### Layout System
|
247
|
+
|
248
|
+
```ruby
|
249
|
+
class LayoutDirectiveProcessor
|
250
|
+
def self.register_layout_directives(processor)
|
251
|
+
processor.register_directive('layout') do |layout_name, context|
|
252
|
+
layout_content = processor.storage.read("layouts/#{layout_name}")
|
253
|
+
context[:current_layout] = layout_content
|
254
|
+
context[:sections] ||= {}
|
255
|
+
'' # Layout directive doesn't output content directly
|
256
|
+
end
|
257
|
+
|
258
|
+
processor.register_directive('section') do |args, context|
|
259
|
+
section_name, content = args.split(':', 2)
|
260
|
+
context[:sections] ||= {}
|
261
|
+
context[:sections][section_name.strip] = content.strip
|
262
|
+
'' # Section directive stores content for later use
|
263
|
+
end
|
264
|
+
|
265
|
+
processor.register_directive('yield') do |section_name, context|
|
266
|
+
section_content = context.dig(:sections, section_name.strip) || ''
|
267
|
+
processor.process(section_content, context)
|
268
|
+
end
|
269
|
+
|
270
|
+
processor.register_directive('render_layout') do |args, context|
|
271
|
+
return '' unless context[:current_layout]
|
272
|
+
|
273
|
+
processor.process(context[:current_layout], context)
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
LayoutDirectiveProcessor.register_layout_directives(
|
279
|
+
PromptManager.configuration.directive_processor
|
280
|
+
)
|
281
|
+
```
|
282
|
+
|
283
|
+
```text
|
284
|
+
# child_template.txt:
|
285
|
+
//layout email_layout
|
286
|
+
//section title: Important Account Update
|
287
|
+
//section content: Your account settings have been updated successfully.
|
288
|
+
//render_layout
|
289
|
+
|
290
|
+
# layouts/email_layout.txt:
|
291
|
+
Subject: //yield title
|
292
|
+
|
293
|
+
Dear [CUSTOMER_NAME],
|
294
|
+
|
295
|
+
//yield content
|
296
|
+
|
297
|
+
Best regards,
|
298
|
+
The Support Team
|
299
|
+
```
|
300
|
+
|
301
|
+
### Component System
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
config.directive_processor.register_directive('component') do |args, context|
|
305
|
+
component_name, props_str = args.split(':', 2)
|
306
|
+
|
307
|
+
# Parse component props
|
308
|
+
props = {}
|
309
|
+
if props_str
|
310
|
+
props_str.scan(/(\w+)="([^"]*)"/) do |key, value|
|
311
|
+
props[key.to_sym] = value
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# Load component template
|
316
|
+
component_template = processor.storage.read("components/#{component_name}")
|
317
|
+
|
318
|
+
# Create component context
|
319
|
+
component_context = context.deep_dup
|
320
|
+
component_context[:parameters].merge!(props)
|
321
|
+
|
322
|
+
# Render component
|
323
|
+
processor.process(component_template, component_context)
|
324
|
+
rescue PromptManager::PromptNotFoundError
|
325
|
+
"<!-- Component '#{component_name}' not found -->"
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
```text
|
330
|
+
# Using components:
|
331
|
+
//component button: text="Click Here" url="https://example.com" style="primary"
|
332
|
+
//component user_card: name="[USER_NAME]" email="[USER_EMAIL]"
|
333
|
+
|
334
|
+
# components/button.txt:
|
335
|
+
[Click here]([URL]) <!-- [TEXT] -->
|
336
|
+
|
337
|
+
# components/user_card.txt:
|
338
|
+
**[NAME]**
|
339
|
+
Email: [EMAIL]
|
340
|
+
```
|
341
|
+
|
342
|
+
## External Service Integration
|
343
|
+
|
344
|
+
### CRM Integration Directive
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
config.directive_processor.register_directive('crm_data') do |args, context|
|
348
|
+
data_type, customer_id = args.split(':', 2)
|
349
|
+
|
350
|
+
case data_type.strip
|
351
|
+
when 'customer_info'
|
352
|
+
customer = CRMService.get_customer(customer_id)
|
353
|
+
<<~INFO
|
354
|
+
Customer: #{customer.name}
|
355
|
+
Account Value: #{customer.lifetime_value}
|
356
|
+
Support Tier: #{customer.support_tier}
|
357
|
+
INFO
|
358
|
+
|
359
|
+
when 'recent_interactions'
|
360
|
+
interactions = CRMService.get_recent_interactions(customer_id, limit: 5)
|
361
|
+
interactions.map do |interaction|
|
362
|
+
"- #{interaction.date}: #{interaction.type} - #{interaction.summary}"
|
363
|
+
end.join("\n")
|
364
|
+
|
365
|
+
else
|
366
|
+
'Unknown CRM data type'
|
367
|
+
end
|
368
|
+
rescue => e
|
369
|
+
Rails.logger.error "CRM integration error: #{e.message}"
|
370
|
+
'CRM data temporarily unavailable'
|
371
|
+
end
|
372
|
+
```
|
373
|
+
|
374
|
+
### Analytics Directive
|
375
|
+
|
376
|
+
```ruby
|
377
|
+
config.directive_processor.register_directive('analytics') do |args, context|
|
378
|
+
metric_type, time_period = args.split(':', 2)
|
379
|
+
time_period ||= '30d'
|
380
|
+
|
381
|
+
case metric_type.strip
|
382
|
+
when 'user_activity'
|
383
|
+
user_id = context.dig(:parameters, :user_id)
|
384
|
+
activity = AnalyticsService.get_user_activity(user_id, time_period)
|
385
|
+
"Active #{activity[:active_days]} days in the last #{time_period}"
|
386
|
+
|
387
|
+
when 'feature_usage'
|
388
|
+
feature_usage = AnalyticsService.get_feature_usage(time_period)
|
389
|
+
top_features = feature_usage.first(3)
|
390
|
+
"Top features: #{top_features.map(&:name).join(', ')}"
|
391
|
+
|
392
|
+
else
|
393
|
+
'Unknown analytics metric'
|
394
|
+
end
|
395
|
+
end
|
396
|
+
```
|
397
|
+
|
398
|
+
## Performance Optimization
|
399
|
+
|
400
|
+
### Caching Dynamic Directives
|
401
|
+
|
402
|
+
```ruby
|
403
|
+
class CachedDirectiveProcessor < PromptManager::DirectiveProcessor
|
404
|
+
def initialize(**options)
|
405
|
+
super(**options)
|
406
|
+
@directive_cache = Rails.cache
|
407
|
+
end
|
408
|
+
|
409
|
+
def register_cached_directive(name, cache_ttl: 300, &handler)
|
410
|
+
cached_handler = lambda do |args, context|
|
411
|
+
cache_key = generate_cache_key(name, args, context)
|
412
|
+
|
413
|
+
@directive_cache.fetch(cache_key, expires_in: cache_ttl) do
|
414
|
+
handler.call(args, context)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
register_directive(name, &cached_handler)
|
419
|
+
end
|
420
|
+
|
421
|
+
private
|
422
|
+
|
423
|
+
def generate_cache_key(directive_name, args, context)
|
424
|
+
relevant_params = context[:parameters].slice(:user_id, :tenant_id)
|
425
|
+
"directive:#{directive_name}:#{args}:#{relevant_params.hash}"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
```
|
429
|
+
|
430
|
+
### Async Directive Processing
|
431
|
+
|
432
|
+
```ruby
|
433
|
+
config.directive_processor.register_directive('async_load') do |args, context|
|
434
|
+
data_source = args.strip
|
435
|
+
cache_key = "async_data:#{data_source}:#{context[:parameters][:user_id]}"
|
436
|
+
|
437
|
+
# Try to get cached result first
|
438
|
+
cached_result = Rails.cache.read(cache_key)
|
439
|
+
return cached_result if cached_result
|
440
|
+
|
441
|
+
# Start async job if not cached
|
442
|
+
AsyncDataLoadJob.perform_later(data_source, cache_key, context[:parameters])
|
443
|
+
|
444
|
+
# Return placeholder
|
445
|
+
"Loading #{data_source} data..."
|
446
|
+
end
|
447
|
+
|
448
|
+
class AsyncDataLoadJob < ApplicationJob
|
449
|
+
def perform(data_source, cache_key, parameters)
|
450
|
+
result = load_data_from_source(data_source, parameters)
|
451
|
+
Rails.cache.write(cache_key, result, expires_in: 1.hour)
|
452
|
+
end
|
453
|
+
end
|
454
|
+
```
|
455
|
+
|
456
|
+
## Testing Dynamic Directives
|
457
|
+
|
458
|
+
### RSpec Examples
|
459
|
+
|
460
|
+
```ruby
|
461
|
+
describe 'Dynamic Directives' do
|
462
|
+
let(:processor) { PromptManager::DirectiveProcessor.new(storage: storage) }
|
463
|
+
let(:storage) { instance_double(PromptManager::Storage::Base) }
|
464
|
+
|
465
|
+
before do
|
466
|
+
processor.register_directive('test_directive') do |args, context|
|
467
|
+
"processed: #{args} with #{context.dig(:parameters, :test_param)}"
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
it 'processes directive with context' do
|
472
|
+
content = "//test_directive hello world"
|
473
|
+
context = { parameters: { test_param: 'value' } }
|
474
|
+
|
475
|
+
result = processor.process(content, context)
|
476
|
+
expect(result).to eq "processed: hello world with value"
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'handles directive errors' do
|
480
|
+
processor.register_directive('error_directive') do |args, context|
|
481
|
+
raise StandardError, 'test error'
|
482
|
+
end
|
483
|
+
|
484
|
+
expect {
|
485
|
+
processor.process("//error_directive test")
|
486
|
+
}.to raise_error(PromptManager::DirectiveProcessingError)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
```
|
490
|
+
|
491
|
+
### Integration Tests
|
492
|
+
|
493
|
+
```ruby
|
494
|
+
describe 'Dynamic Directive Integration' do
|
495
|
+
it 'processes complex directive chains' do
|
496
|
+
storage = PromptManager::Storage::FileSystemAdapter.new(prompts_dir: 'spec/fixtures')
|
497
|
+
processor = PromptManager::DirectiveProcessor.new(storage: storage)
|
498
|
+
|
499
|
+
# Register test directives
|
500
|
+
processor.register_directive('if') { |condition, context| ... }
|
501
|
+
processor.register_directive('foreach') { |args, context| ... }
|
502
|
+
|
503
|
+
prompt_content = <<~PROMPT
|
504
|
+
//if [USER_TYPE] == 'premium'
|
505
|
+
Premium Features:
|
506
|
+
//foreach features: - [ITEM.name]: [ITEM.description]
|
507
|
+
//endif
|
508
|
+
PROMPT
|
509
|
+
|
510
|
+
result = processor.process(prompt_content, {
|
511
|
+
parameters: {
|
512
|
+
user_type: 'premium',
|
513
|
+
features: [
|
514
|
+
{ name: 'Feature 1', description: 'Description 1' },
|
515
|
+
{ name: 'Feature 2', description: 'Description 2' }
|
516
|
+
]
|
517
|
+
}
|
518
|
+
})
|
519
|
+
|
520
|
+
expect(result).to include('Feature 1: Description 1')
|
521
|
+
expect(result).to include('Feature 2: Description 2')
|
522
|
+
end
|
523
|
+
end
|
524
|
+
```
|
525
|
+
|
526
|
+
## Best Practices
|
527
|
+
|
528
|
+
1. **Error Handling**: Always handle errors gracefully in directive processors
|
529
|
+
2. **Performance**: Cache expensive operations and use async processing when appropriate
|
530
|
+
3. **Security**: Validate and sanitize all directive arguments
|
531
|
+
4. **Context Isolation**: Be careful when modifying context to avoid side effects
|
532
|
+
5. **Documentation**: Document directive syntax, parameters, and behavior
|
533
|
+
6. **Testing**: Write comprehensive tests including error cases
|
534
|
+
7. **Naming**: Use clear, descriptive names that indicate the directive's purpose
|
535
|
+
8. **Resource Management**: Clean up resources and connections properly
|