cadence-ruby 0.0.0 → 0.1.0
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/README.md +456 -0
- data/cadence.gemspec +9 -2
- data/lib/cadence-ruby.rb +1 -0
- data/lib/cadence.rb +176 -0
- data/lib/cadence/activity.rb +33 -0
- data/lib/cadence/activity/async_token.rb +34 -0
- data/lib/cadence/activity/context.rb +64 -0
- data/lib/cadence/activity/poller.rb +89 -0
- data/lib/cadence/activity/task_processor.rb +73 -0
- data/lib/cadence/activity/workflow_convenience_methods.rb +41 -0
- data/lib/cadence/client.rb +21 -0
- data/lib/cadence/client/errors.rb +8 -0
- data/lib/cadence/client/thrift_client.rb +380 -0
- data/lib/cadence/concerns/executable.rb +33 -0
- data/lib/cadence/concerns/typed.rb +40 -0
- data/lib/cadence/configuration.rb +36 -0
- data/lib/cadence/errors.rb +21 -0
- data/lib/cadence/executable_lookup.rb +25 -0
- data/lib/cadence/execution_options.rb +32 -0
- data/lib/cadence/json.rb +18 -0
- data/lib/cadence/metadata.rb +73 -0
- data/lib/cadence/metadata/activity.rb +28 -0
- data/lib/cadence/metadata/base.rb +17 -0
- data/lib/cadence/metadata/decision.rb +25 -0
- data/lib/cadence/metadata/workflow.rb +23 -0
- data/lib/cadence/metrics.rb +37 -0
- data/lib/cadence/metrics_adapters/log.rb +33 -0
- data/lib/cadence/metrics_adapters/null.rb +9 -0
- data/lib/cadence/middleware/chain.rb +30 -0
- data/lib/cadence/middleware/entry.rb +9 -0
- data/lib/cadence/retry_policy.rb +27 -0
- data/lib/cadence/saga/concern.rb +37 -0
- data/lib/cadence/saga/result.rb +22 -0
- data/lib/cadence/saga/saga.rb +24 -0
- data/lib/cadence/testing.rb +50 -0
- data/lib/cadence/testing/cadence_override.rb +112 -0
- data/lib/cadence/testing/future_registry.rb +27 -0
- data/lib/cadence/testing/local_activity_context.rb +17 -0
- data/lib/cadence/testing/local_workflow_context.rb +207 -0
- data/lib/cadence/testing/workflow_execution.rb +44 -0
- data/lib/cadence/testing/workflow_override.rb +36 -0
- data/lib/cadence/thread_local_context.rb +14 -0
- data/lib/cadence/thread_pool.rb +68 -0
- data/lib/cadence/types.rb +7 -0
- data/lib/cadence/utils.rb +17 -0
- data/lib/cadence/uuid.rb +19 -0
- data/lib/cadence/version.rb +1 -1
- data/lib/cadence/worker.rb +91 -0
- data/lib/cadence/workflow.rb +42 -0
- data/lib/cadence/workflow/context.rb +266 -0
- data/lib/cadence/workflow/convenience_methods.rb +34 -0
- data/lib/cadence/workflow/decision.rb +39 -0
- data/lib/cadence/workflow/decision_state_machine.rb +48 -0
- data/lib/cadence/workflow/decision_task_processor.rb +105 -0
- data/lib/cadence/workflow/dispatcher.rb +31 -0
- data/lib/cadence/workflow/execution_info.rb +45 -0
- data/lib/cadence/workflow/executor.rb +45 -0
- data/lib/cadence/workflow/future.rb +75 -0
- data/lib/cadence/workflow/history.rb +76 -0
- data/lib/cadence/workflow/history/event.rb +71 -0
- data/lib/cadence/workflow/history/event_target.rb +79 -0
- data/lib/cadence/workflow/history/window.rb +40 -0
- data/lib/cadence/workflow/poller.rb +74 -0
- data/lib/cadence/workflow/replay_aware_logger.rb +36 -0
- data/lib/cadence/workflow/serializer.rb +31 -0
- data/lib/cadence/workflow/serializer/base.rb +22 -0
- data/lib/cadence/workflow/serializer/cancel_timer.rb +19 -0
- data/lib/cadence/workflow/serializer/complete_workflow.rb +20 -0
- data/lib/cadence/workflow/serializer/fail_workflow.rb +21 -0
- data/lib/cadence/workflow/serializer/record_marker.rb +21 -0
- data/lib/cadence/workflow/serializer/request_activity_cancellation.rb +19 -0
- data/lib/cadence/workflow/serializer/schedule_activity.rb +54 -0
- data/lib/cadence/workflow/serializer/start_child_workflow.rb +52 -0
- data/lib/cadence/workflow/serializer/start_timer.rb +20 -0
- data/lib/cadence/workflow/state_manager.rb +324 -0
- data/lib/gen/thrift/cadence_constants.rb +11 -0
- data/lib/gen/thrift/cadence_types.rb +11 -0
- data/lib/gen/thrift/shared_constants.rb +11 -0
- data/lib/gen/thrift/shared_types.rb +4600 -0
- data/lib/gen/thrift/workflow_service.rb +3142 -0
- data/rbi/cadence-ruby.rbi +39 -0
- metadata +152 -5
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'cadence/utils'
|
2
|
+
|
3
|
+
module Cadence
|
4
|
+
class Workflow
|
5
|
+
class History
|
6
|
+
class Event
|
7
|
+
EVENT_TYPES = %w[
|
8
|
+
ActivityTaskStarted
|
9
|
+
ActivityTaskCompleted
|
10
|
+
ActivityTaskFailed
|
11
|
+
ActivityTaskTimedOut
|
12
|
+
ActivityTaskCanceled
|
13
|
+
TimerFired
|
14
|
+
RequestCancelExternalWorkflowExecutionFailed
|
15
|
+
WorkflowExecutionSignaled
|
16
|
+
WorkflowExecutionTerminated
|
17
|
+
SignalExternalWorkflowExecutionFailed
|
18
|
+
ExternalWorkflowExecutionCancelRequested
|
19
|
+
ExternalWorkflowExecutionSignaled
|
20
|
+
UpsertWorkflowSearchAttributes
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
CHILD_WORKFLOW_EVENTS = %w[
|
24
|
+
StartChildWorkflowExecutionFailed
|
25
|
+
ChildWorkflowExecutionStarted
|
26
|
+
ChildWorkflowExecutionCompleted
|
27
|
+
ChildWorkflowExecutionFailed
|
28
|
+
ChildWorkflowExecutionCanceled
|
29
|
+
ChildWorkflowExecutionTimedOut
|
30
|
+
ChildWorkflowExecutionTerminated
|
31
|
+
].freeze
|
32
|
+
|
33
|
+
attr_reader :id, :timestamp, :type, :attributes
|
34
|
+
|
35
|
+
def initialize(raw_event)
|
36
|
+
@id = raw_event.eventId
|
37
|
+
@timestamp = Utils.time_from_nanos(raw_event.timestamp)
|
38
|
+
@type = CadenceThrift::EventType::VALUE_MAP[raw_event.eventType]
|
39
|
+
@attributes = extract_attributes(raw_event)
|
40
|
+
|
41
|
+
freeze
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the ID of the first event associated with the current event,
|
45
|
+
# referred to as a "decision" event. Not related to DecisionTask.
|
46
|
+
def decision_id
|
47
|
+
case type
|
48
|
+
when 'TimerFired'
|
49
|
+
attributes.startedEventId
|
50
|
+
when 'WorkflowExecutionSignaled'
|
51
|
+
1 # fixed id for everything related to current workflow
|
52
|
+
when *EVENT_TYPES
|
53
|
+
attributes.scheduledEventId
|
54
|
+
when *CHILD_WORKFLOW_EVENTS
|
55
|
+
attributes.initiatedEventId
|
56
|
+
else
|
57
|
+
id
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def extract_attributes(raw_event)
|
64
|
+
attributes_argument = "#{type}EventAttributes"
|
65
|
+
attributes_argument[0] = attributes_argument[0].downcase
|
66
|
+
raw_event.public_send(attributes_argument)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'cadence/errors'
|
2
|
+
|
3
|
+
module Cadence
|
4
|
+
class Workflow
|
5
|
+
class History
|
6
|
+
class EventTarget
|
7
|
+
class UnexpectedEventType < InternalError; end
|
8
|
+
|
9
|
+
ACTIVITY_TYPE = :activity
|
10
|
+
CANCEL_ACTIVITY_REQUEST_TYPE = :cancel_activity_request
|
11
|
+
TIMER_TYPE = :timer
|
12
|
+
CANCEL_TIMER_REQUEST_TYPE = :cancel_timer_request
|
13
|
+
CHILD_WORKFLOW_TYPE = :child_workflow
|
14
|
+
MARKER_TYPE = :marker
|
15
|
+
EXTERNAL_WORKFLOW_TYPE = :external_workflow
|
16
|
+
CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE = :cancel_external_workflow_request
|
17
|
+
WORKFLOW_TYPE = :workflow
|
18
|
+
CANCEL_WORKFLOW_REQUEST_TYPE = :cancel_workflow_request
|
19
|
+
|
20
|
+
TARGET_TYPES = {
|
21
|
+
'ActivityTask' => ACTIVITY_TYPE,
|
22
|
+
'ActivityTaskCancel' => CANCEL_ACTIVITY_REQUEST_TYPE,
|
23
|
+
'RequestCancelActivityTask' => CANCEL_ACTIVITY_REQUEST_TYPE,
|
24
|
+
'Timer' => TIMER_TYPE,
|
25
|
+
'CancelTimer' => CANCEL_TIMER_REQUEST_TYPE,
|
26
|
+
'ChildWorkflowExecution' => CHILD_WORKFLOW_TYPE,
|
27
|
+
'StartChildWorkflowExecution' => CHILD_WORKFLOW_TYPE,
|
28
|
+
'Marker' => MARKER_TYPE,
|
29
|
+
'ExternalWorkflowExecution' => EXTERNAL_WORKFLOW_TYPE,
|
30
|
+
'SignalExternalWorkflowExecution' => EXTERNAL_WORKFLOW_TYPE,
|
31
|
+
'ExternalWorkflowExecutionCancel' => CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE,
|
32
|
+
'RequestCancelExternalWorkflowExecution' => CANCEL_EXTERNAL_WORKFLOW_REQUEST_TYPE,
|
33
|
+
'UpsertWorkflowSearchAttributes' => WORKFLOW_TYPE,
|
34
|
+
'WorkflowExecution' => WORKFLOW_TYPE,
|
35
|
+
'WorkflowExecutionCancel' => CANCEL_WORKFLOW_REQUEST_TYPE,
|
36
|
+
}.freeze
|
37
|
+
|
38
|
+
attr_reader :id, :type
|
39
|
+
|
40
|
+
def self.workflow
|
41
|
+
@workflow ||= new(1, WORKFLOW_TYPE)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.from_event(event)
|
45
|
+
_, target_type = TARGET_TYPES.find { |type, _| event.type.start_with?(type) }
|
46
|
+
|
47
|
+
unless target_type
|
48
|
+
raise UnexpectedEventType, "Unexpected event #{event.type}"
|
49
|
+
end
|
50
|
+
|
51
|
+
new(event.decision_id, target_type)
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(id, type)
|
55
|
+
@id = id
|
56
|
+
@type = type
|
57
|
+
|
58
|
+
freeze
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
id == other.id && type == other.type
|
63
|
+
end
|
64
|
+
|
65
|
+
def eql?(other)
|
66
|
+
self == other
|
67
|
+
end
|
68
|
+
|
69
|
+
def hash
|
70
|
+
[id, type].hash
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_s
|
74
|
+
"#{type} (#{id})"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Cadence
|
2
|
+
class Workflow
|
3
|
+
class History
|
4
|
+
class Window
|
5
|
+
attr_reader :local_time, :last_event_id, :events, :markers
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@local_time = nil
|
9
|
+
@last_event_id = nil
|
10
|
+
@events = []
|
11
|
+
@markers = []
|
12
|
+
@replay = false
|
13
|
+
end
|
14
|
+
|
15
|
+
def replay?
|
16
|
+
@replay
|
17
|
+
end
|
18
|
+
|
19
|
+
def add(event)
|
20
|
+
case event.type
|
21
|
+
when 'MarkerRecorded'
|
22
|
+
markers << event
|
23
|
+
when 'DecisionTaskStarted'
|
24
|
+
@last_event_id = event.id + 1 # one for completed
|
25
|
+
@local_time = event.timestamp
|
26
|
+
when 'DecisionTaskFailed', 'DecisionTaskTimedOut'
|
27
|
+
@next_event_id = nil
|
28
|
+
@local_time = nil
|
29
|
+
when 'DecisionTaskCompleted'
|
30
|
+
@replay = true
|
31
|
+
when 'DecisionTaskScheduled', 'DecisionTaskFailed'
|
32
|
+
# no-op
|
33
|
+
else
|
34
|
+
events << event
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'cadence/client'
|
2
|
+
require 'cadence/middleware/chain'
|
3
|
+
require 'cadence/workflow/decision_task_processor'
|
4
|
+
|
5
|
+
module Cadence
|
6
|
+
class Workflow
|
7
|
+
class Poller
|
8
|
+
def initialize(domain, task_list, workflow_lookup, middleware = [], options = {})
|
9
|
+
@domain = domain
|
10
|
+
@task_list = task_list
|
11
|
+
@workflow_lookup = workflow_lookup
|
12
|
+
@middleware = middleware
|
13
|
+
@options = options
|
14
|
+
@shutting_down = false
|
15
|
+
end
|
16
|
+
|
17
|
+
def start
|
18
|
+
@shutting_down = false
|
19
|
+
@thread = Thread.new(&method(:poll_loop))
|
20
|
+
end
|
21
|
+
|
22
|
+
def stop
|
23
|
+
@shutting_down = true
|
24
|
+
Cadence.logger.info('Shutting down a workflow poller')
|
25
|
+
end
|
26
|
+
|
27
|
+
def wait
|
28
|
+
@thread.join
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :domain, :task_list, :client, :workflow_lookup, :middleware, :options
|
34
|
+
|
35
|
+
def client
|
36
|
+
@client ||= Cadence::Client.generate(options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def middleware_chain
|
40
|
+
@middleware_chain ||= Middleware::Chain.new(middleware)
|
41
|
+
end
|
42
|
+
|
43
|
+
def shutting_down?
|
44
|
+
@shutting_down
|
45
|
+
end
|
46
|
+
|
47
|
+
def poll_loop
|
48
|
+
last_poll_time = Time.now
|
49
|
+
metrics_tags = { domain: domain, task_list: task_list }.freeze
|
50
|
+
|
51
|
+
while !shutting_down? do
|
52
|
+
time_diff_ms = ((Time.now - last_poll_time) * 1000).round
|
53
|
+
Cadence.metrics.timing('workflow_poller.time_since_last_poll', time_diff_ms, metrics_tags)
|
54
|
+
Cadence.logger.debug("Polling for decision tasks (#{domain} / #{task_list})")
|
55
|
+
|
56
|
+
task = poll_for_task
|
57
|
+
last_poll_time = Time.now
|
58
|
+
process(task) if task&.workflowType
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def poll_for_task
|
63
|
+
client.poll_for_decision_task(domain: domain, task_list: task_list)
|
64
|
+
rescue StandardError => error
|
65
|
+
Cadence.logger.error("Unable to poll for a decision task: #{error.inspect}")
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def process(task)
|
70
|
+
DecisionTaskProcessor.new(task, domain, workflow_lookup, client, middleware_chain).process
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Cadence
|
2
|
+
class Workflow
|
3
|
+
class ReplayAwareLogger
|
4
|
+
SEVERITIES = %i[debug info warn error fatal unknown].freeze
|
5
|
+
|
6
|
+
attr_writer :replay
|
7
|
+
|
8
|
+
def initialize(main_logger, replay = true)
|
9
|
+
@main_logger = main_logger
|
10
|
+
@replay = replay
|
11
|
+
end
|
12
|
+
|
13
|
+
SEVERITIES.each do |severity|
|
14
|
+
define_method severity do |message|
|
15
|
+
return if replay?
|
16
|
+
|
17
|
+
main_logger.public_send(severity, message)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def log(severity, message)
|
22
|
+
return if replay?
|
23
|
+
|
24
|
+
main_logger.log(severity, message)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :main_logger
|
30
|
+
|
31
|
+
def replay?
|
32
|
+
@replay
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'cadence/workflow/decision'
|
2
|
+
require 'cadence/workflow/serializer/schedule_activity'
|
3
|
+
require 'cadence/workflow/serializer/start_child_workflow'
|
4
|
+
require 'cadence/workflow/serializer/request_activity_cancellation'
|
5
|
+
require 'cadence/workflow/serializer/record_marker'
|
6
|
+
require 'cadence/workflow/serializer/start_timer'
|
7
|
+
require 'cadence/workflow/serializer/cancel_timer'
|
8
|
+
require 'cadence/workflow/serializer/complete_workflow'
|
9
|
+
require 'cadence/workflow/serializer/fail_workflow'
|
10
|
+
|
11
|
+
module Cadence
|
12
|
+
class Workflow
|
13
|
+
module Serializer
|
14
|
+
SERIALIZERS_MAP = {
|
15
|
+
Workflow::Decision::ScheduleActivity => Serializer::ScheduleActivity,
|
16
|
+
Workflow::Decision::StartChildWorkflow => Serializer::StartChildWorkflow,
|
17
|
+
Workflow::Decision::RequestActivityCancellation => Serializer::RequestActivityCancellation,
|
18
|
+
Workflow::Decision::RecordMarker => Serializer::RecordMarker,
|
19
|
+
Workflow::Decision::StartTimer => Serializer::StartTimer,
|
20
|
+
Workflow::Decision::CancelTimer => Serializer::CancelTimer,
|
21
|
+
Workflow::Decision::CompleteWorkflow => Serializer::CompleteWorkflow,
|
22
|
+
Workflow::Decision::FailWorkflow => Serializer::FailWorkflow
|
23
|
+
}.freeze
|
24
|
+
|
25
|
+
def self.serialize(object)
|
26
|
+
serializer = SERIALIZERS_MAP[object.class]
|
27
|
+
serializer.new(object).to_thrift
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'oj'
|
2
|
+
require 'gen/thrift/shared_types'
|
3
|
+
|
4
|
+
module Cadence
|
5
|
+
class Workflow
|
6
|
+
module Serializer
|
7
|
+
class Base
|
8
|
+
def initialize(object)
|
9
|
+
@object = object
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_thrift
|
13
|
+
raise NotImplementedError, 'serializer needs to implement #to_thrift'
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
attr_reader :object
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'cadence/workflow/serializer/base'
|
2
|
+
|
3
|
+
module Cadence
|
4
|
+
class Workflow
|
5
|
+
module Serializer
|
6
|
+
class CancelTimer < Base
|
7
|
+
def to_thrift
|
8
|
+
CadenceThrift::Decision.new(
|
9
|
+
decisionType: CadenceThrift::DecisionType::CancelTimer,
|
10
|
+
cancelTimerDecisionAttributes:
|
11
|
+
CadenceThrift::CancelTimerDecisionAttributes.new(
|
12
|
+
timerId: object.timer_id.to_s
|
13
|
+
)
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'cadence/workflow/serializer/base'
|
2
|
+
require 'cadence/json'
|
3
|
+
|
4
|
+
module Cadence
|
5
|
+
class Workflow
|
6
|
+
module Serializer
|
7
|
+
class CompleteWorkflow < Base
|
8
|
+
def to_thrift
|
9
|
+
CadenceThrift::Decision.new(
|
10
|
+
decisionType: CadenceThrift::DecisionType::CompleteWorkflowExecution,
|
11
|
+
completeWorkflowExecutionDecisionAttributes:
|
12
|
+
CadenceThrift::CompleteWorkflowExecutionDecisionAttributes.new(
|
13
|
+
result: JSON.serialize(object.result)
|
14
|
+
)
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'cadence/workflow/serializer/base'
|
2
|
+
require 'cadence/json'
|
3
|
+
|
4
|
+
module Cadence
|
5
|
+
class Workflow
|
6
|
+
module Serializer
|
7
|
+
class FailWorkflow < Base
|
8
|
+
def to_thrift
|
9
|
+
CadenceThrift::Decision.new(
|
10
|
+
decisionType: CadenceThrift::DecisionType::FailWorkflowExecution,
|
11
|
+
failWorkflowExecutionDecisionAttributes:
|
12
|
+
CadenceThrift::FailWorkflowExecutionDecisionAttributes.new(
|
13
|
+
reason: object.reason,
|
14
|
+
details: JSON.serialize(object.details)
|
15
|
+
)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'cadence/workflow/serializer/base'
|
2
|
+
require 'cadence/json'
|
3
|
+
|
4
|
+
module Cadence
|
5
|
+
class Workflow
|
6
|
+
module Serializer
|
7
|
+
class RecordMarker < Base
|
8
|
+
def to_thrift
|
9
|
+
CadenceThrift::Decision.new(
|
10
|
+
decisionType: CadenceThrift::DecisionType::RecordMarker,
|
11
|
+
recordMarkerDecisionAttributes:
|
12
|
+
CadenceThrift::RecordMarkerDecisionAttributes.new(
|
13
|
+
markerName: object.name,
|
14
|
+
details: JSON.serialize(object.details)
|
15
|
+
)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|