caset 1.0.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 +7 -0
- data/README.md +180 -0
- data/bin/cm +15 -0
- data/bin/cml +15 -0
- data/doc/MANUAL.md +100 -0
- data/doc/sample.csv +12 -0
- data/doc/sample.md +60 -0
- data/lib/caset/gen_bdd.rb +121 -0
- data/lib/caset/gen_caset.rb +41 -0
- data/lib/caset/gen_pytest.rb +211 -0
- data/lib/caset/gen_spec.rb +36 -0
- data/lib/caset/version.rb +3 -0
- data/lib/caset.rb +272 -0
- data/lib/casetdown/casetable.rb +121 -0
- data/lib/casetdown/casetcode.rb +345 -0
- data/lib/casetdown/casetdoc.rb +241 -0
- data/lib/casetdown/casetter.rb +141 -0
- data/lib/casetdown/cli.rb +129 -0
- data/lib/casetdown/live.rb +102 -0
- data/lib/casetdown/support/api_app.rb +127 -0
- data/lib/casetdown/support/endata.rb +197 -0
- data/lib/casetdown/support/simtab.rb +372 -0
- data/lib/casetdown/support/text_abstract.rb +124 -0
- data/lib/casetdown/support/tintext.rb +141 -0
- data/lib/casetdown.rb +48 -0
- metadata +79 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
|
|
3
|
+
#------------------------------------------------------------------------------
|
|
4
|
+
# Markdown 解析器加载与包装器
|
|
5
|
+
#------------------------------------------------------------------------------
|
|
6
|
+
module CasetDown
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# casetdown 库根目录(lib/)。
|
|
10
|
+
#
|
|
11
|
+
# 供代码执行引擎以 `ruby -I"..." ./~tmp.rb` 启动子进程时定位
|
|
12
|
+
# require "casetdown",使仓库直跑与 gem 安装两种方式下,
|
|
13
|
+
# 执行脚本中的 EnData / SimTab / TinText 数据绑定均可用。
|
|
14
|
+
#
|
|
15
|
+
# @return [String] lib 目录的绝对路径
|
|
16
|
+
def home
|
|
17
|
+
File.expand_path('..', __dir__)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Loader
|
|
21
|
+
# 加载指定 Markdown 文件并解析为结构化数据。
|
|
22
|
+
#
|
|
23
|
+
# @param path [String] Markdown 文件路径
|
|
24
|
+
# @param option [Hash] 可选参数,会合并到解析参数中
|
|
25
|
+
# @option option [Class] :parser 解析器类,默认为 ReDiscount(manticore)
|
|
26
|
+
# @option option [Boolean] :doc 是否返回原始文档对象,默认为 true
|
|
27
|
+
# @option option [Boolean] :tree 是否返回树结构,默认为 true
|
|
28
|
+
# @option option [Boolean] :src 是否返回源节点列表,默认为 true
|
|
29
|
+
# @return [Hash] 解析结果,包含 :path, :src, :tree, :doc 等键
|
|
30
|
+
# @example
|
|
31
|
+
# result = CasetDown.load("README.md")
|
|
32
|
+
# result[:doc].text # 获取原始文本
|
|
33
|
+
def load path, option={}
|
|
34
|
+
args = {path: path}.merge(option)
|
|
35
|
+
text = (File.read path).gsub("\r","").split("\n").map{|line|line.rstrip}.join("\n")
|
|
36
|
+
return self.parse(text, args)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# InlineStructs
|
|
40
|
+
# 将 Markdown 文本字符串解析为结构化数据。
|
|
41
|
+
#
|
|
42
|
+
# @param text [String] Markdown 原始文本
|
|
43
|
+
# @param option [Hash] 可选参数
|
|
44
|
+
# @option option [Class] :parser 解析器类,默认为 RDiscount
|
|
45
|
+
# @option option [Boolean] :doc 是否返回原始文档对象,默认为 true
|
|
46
|
+
# @option option [Boolean] :tree 是否返回树结构,默认为 true
|
|
47
|
+
# @option option [Boolean] :src 是否返回源节点列表,默认为 true
|
|
48
|
+
# @option option [String] :path 文件路径标识,默认为 'tmp'
|
|
49
|
+
# @return [Hash] 解析结果
|
|
50
|
+
# - :path [String] 文件路径
|
|
51
|
+
# - :src [Script] 原始节点列表包装器
|
|
52
|
+
# - :tree [Tree] 按标签分组的节点树
|
|
53
|
+
# - :doc [RDiscount] 原始文档对象
|
|
54
|
+
def parse text, option={}
|
|
55
|
+
args = {
|
|
56
|
+
parser: ReDiscount, # parser := {Kramdown::Document | RDiscount} ★ Kramdown::Document无法处理好多重空行问题
|
|
57
|
+
doc: true,
|
|
58
|
+
tree: true,
|
|
59
|
+
src: true,
|
|
60
|
+
path: 'tmp'
|
|
61
|
+
}.merge(option)
|
|
62
|
+
document = args[:parser].new(text)
|
|
63
|
+
html = "<html>"+document.to_html+"</html>"
|
|
64
|
+
script, tree = [],{}
|
|
65
|
+
XmlParser.parse(html).elements.each do|node|
|
|
66
|
+
doc = node.to_a
|
|
67
|
+
script << node
|
|
68
|
+
tree[doc[0]] ||= []
|
|
69
|
+
tree[doc[0]] << node
|
|
70
|
+
end
|
|
71
|
+
result = {path: args[:path]}
|
|
72
|
+
result[:src] = Script.new(native: script) if args[:src]
|
|
73
|
+
result[:tree] = Tree.new(native: tree) if args[:tree]
|
|
74
|
+
result[:doc] = document if args[:doc]
|
|
75
|
+
return result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 文档树结构包装器,按标签名分组存储 DOM 节点。
|
|
79
|
+
#
|
|
80
|
+
# 内部使用 @content 存储分组后的节点树,键为标签名,值为节点数组。
|
|
81
|
+
class Tree
|
|
82
|
+
# 初始化树结构。
|
|
83
|
+
#
|
|
84
|
+
# @param options [Hash] 初始化参数
|
|
85
|
+
# @option options [Hash] :native 原生树结构数据
|
|
86
|
+
# @option options [String] :json JSON 字符串,会解析为树结构
|
|
87
|
+
# @option options [String] :path 文件路径,通过 CasetDown.load 获取
|
|
88
|
+
# @option options [String] :doc Markdown 文本,通过 CasetDown.parse 获取
|
|
89
|
+
# @return [Tree] 树实例
|
|
90
|
+
def initialize options={}
|
|
91
|
+
@content ||= options[:native]
|
|
92
|
+
@content ||= JSON.parse(options[:json]) if options[:json]
|
|
93
|
+
@content ||= CasetDown.load(options[:path])[-1] if options[:path]
|
|
94
|
+
@content ||= CasetDown.parse(options[:doc])[-1] if options[:doc]
|
|
95
|
+
@content ||= {}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 遍历并返回树内容,支持传入回调函数。
|
|
99
|
+
#
|
|
100
|
+
# @yield [content] 传入树内容,缺省返回内容本身
|
|
101
|
+
# @yieldparam content [Hash] 树结构数据
|
|
102
|
+
# @yieldreturn [Object] 回调处理结果
|
|
103
|
+
# @return [Object] 执行 block.call(@content) 的结果
|
|
104
|
+
def nodes &block
|
|
105
|
+
block = lambda{|content|content} unless block
|
|
106
|
+
block.call(@content)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# 文档节点顺序列表包装器,保持原始文档的节点顺序。
|
|
111
|
+
#
|
|
112
|
+
# 内部使用 @content 存储原始节点数组。
|
|
113
|
+
class Script
|
|
114
|
+
# 初始化节点列表。
|
|
115
|
+
#
|
|
116
|
+
# @param options [Hash] 初始化参数
|
|
117
|
+
# @option options [Array] :native 原生节点数组
|
|
118
|
+
# @option options [String] :json JSON 字符串,会解析为节点数组
|
|
119
|
+
# @option options [String] :path 文件路径,通过 CasetDown.load 获取
|
|
120
|
+
# @option options [String] :doc Markdown 文本,通过 CasetDown.parse 获取
|
|
121
|
+
# @return [Script] 脚本实例
|
|
122
|
+
def initialize options={}
|
|
123
|
+
@content ||= options[:native]
|
|
124
|
+
@content ||= JSON.parse(options[:json]) if options[:json]
|
|
125
|
+
@content ||= CasetDown.load(options[:path])[0] if options[:path]
|
|
126
|
+
@content ||= CasetDown.parse(options[:doc])[0] if options[:doc]
|
|
127
|
+
@content ||= []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 遍历并返回节点列表,支持传入回调函数。
|
|
131
|
+
#
|
|
132
|
+
# @yield [content] 传入节点数组,缺省返回内容本身
|
|
133
|
+
# @yieldparam content [Array] 节点数组
|
|
134
|
+
# @yieldreturn [Object] 回调处理结果
|
|
135
|
+
# @return [Object] 执行 block.call(@content) 的结果
|
|
136
|
+
def stack &block
|
|
137
|
+
block = lambda{|content|content} unless block
|
|
138
|
+
block.call(@content)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
|
|
3
|
+
#------------------------------------------------------------------------------
|
|
4
|
+
# cm 命令实现(CasetDown::CLI)
|
|
5
|
+
#------------------------------------------------------------------------------
|
|
6
|
+
# 对可执行 Markdown 文档做形式化执行与生成:
|
|
7
|
+
# 解析文档结构 -> 导入导出表格 -> 执行代码块 -> 结果回写渲染文件
|
|
8
|
+
#
|
|
9
|
+
# 命令行用法(bin/cm 薄启动器调用 CasetDown::CLI.run):
|
|
10
|
+
#
|
|
11
|
+
# cm [options] <filename>[.md]
|
|
12
|
+
#
|
|
13
|
+
# options:
|
|
14
|
+
# --help 查看帮助
|
|
15
|
+
# --check 检查文档结构
|
|
16
|
+
# --source 查看文档中的源代码
|
|
17
|
+
# --rtab 导入外部表格到文档中
|
|
18
|
+
# --wtab 导出内部表格到外部文件
|
|
19
|
+
# --input 导出源代码文件并渲染文档
|
|
20
|
+
# --output 导出代码运行结果
|
|
21
|
+
# --all 导出源代码和运行结果(input+output)
|
|
22
|
+
# --preview 预览渲染文档(不生成渲染结果)
|
|
23
|
+
# --debug 调试开关, 不删除中间代码
|
|
24
|
+
# --rendfile 渲染文档并生成一个时间戳(默认)
|
|
25
|
+
#------------------------------------------------------------------------------
|
|
26
|
+
module CasetDown
|
|
27
|
+
module CLI
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# CLI 帮助文本。
|
|
31
|
+
#
|
|
32
|
+
# @return [String] 多行帮助信息
|
|
33
|
+
def help
|
|
34
|
+
%Q{cm [options] <filename>[.md]
|
|
35
|
+
|
|
36
|
+
options:
|
|
37
|
+
--help 查看帮助
|
|
38
|
+
--check 检查文档结构
|
|
39
|
+
--source 查看文档中的源代码
|
|
40
|
+
--rtab 导入外部表格到文档中
|
|
41
|
+
--wtab 导出内部表格到外部文件
|
|
42
|
+
--input 导出源代码文件并渲染文档
|
|
43
|
+
--output 导出代码运行结果
|
|
44
|
+
--all 导出源代码和运行结果(input+output)
|
|
45
|
+
--preview 预览渲染文档(不生成渲染结果)
|
|
46
|
+
--debug 调试开关, 不删除中间代码
|
|
47
|
+
--rendfile 渲染文档并生成一个时间戳(默认)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 命令行入口:解析参数并执行对应动作。
|
|
52
|
+
#
|
|
53
|
+
# @param argv [Array<String>] 命令行参数(不含程序名)
|
|
54
|
+
# @return [Integer] 进程退出码
|
|
55
|
+
def run argv
|
|
56
|
+
pool = argv.dup
|
|
57
|
+
check, source = false, false
|
|
58
|
+
|
|
59
|
+
if pool.include?('-h') or pool.include?('--help')
|
|
60
|
+
puts help
|
|
61
|
+
return 0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if pool.include?('--check')
|
|
65
|
+
check = true
|
|
66
|
+
pool.delete '--check'
|
|
67
|
+
end
|
|
68
|
+
if pool.include?('--source')
|
|
69
|
+
source = true
|
|
70
|
+
pool.delete '--source'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
table, file = [], []
|
|
74
|
+
['--rtab','--wtab'].each do|tag|
|
|
75
|
+
if pool.include?(tag)
|
|
76
|
+
table << tag
|
|
77
|
+
pool.delete tag
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
['--input','--output','--all','--preview','--debug'].each do|tag|
|
|
81
|
+
if pool.include?(tag)
|
|
82
|
+
file << tag
|
|
83
|
+
pool.delete tag
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
path = pool.last
|
|
88
|
+
path = Dir["./*.md"].last unless path
|
|
89
|
+
return 0 unless path
|
|
90
|
+
|
|
91
|
+
if File.exist?(path)
|
|
92
|
+
cds = CasetDown.load path
|
|
93
|
+
elsif File.exist?("#{path}.md")
|
|
94
|
+
cds = CasetDown.load "#{path}.md"
|
|
95
|
+
else
|
|
96
|
+
puts "File #{path} not exist."
|
|
97
|
+
return 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if check
|
|
101
|
+
puts "checking>>>>"
|
|
102
|
+
pp CasetDown::Check.all(cds)
|
|
103
|
+
return 0
|
|
104
|
+
end
|
|
105
|
+
if source
|
|
106
|
+
puts "source>>>>"
|
|
107
|
+
pp CasetDown.search_codes(cds)
|
|
108
|
+
return 0
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
render_file cds, table: table.map{|i|i[2..-1].to_sym}, file: file.map{|i|i[2..-1].to_sym}+[:rendfile]
|
|
112
|
+
return 0
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 默认渲染流程:表格导入导出 + 代码执行 + 渲染文件生成。
|
|
116
|
+
#
|
|
117
|
+
# cm 主流程与 cml 监听渲染共用的入口。
|
|
118
|
+
#
|
|
119
|
+
# @param cds [Hash] CasetDown.load 的解析结果
|
|
120
|
+
# @param table [Array<Symbol>] 表格选项(:rtab / :wtab)
|
|
121
|
+
# @param file [Array<Symbol>] 文件选项(:input / :output / :all / :preview / :debug / :rendfile)
|
|
122
|
+
# @return [String] 回写后的完整文档文本
|
|
123
|
+
def render_file cds, table: [], file: [:rendfile]
|
|
124
|
+
tmp = CasetDown.render_table(cds, opt: table)
|
|
125
|
+
CasetDown.run_to_file cds, stream: tmp[:text], data: tmp[:data], fact: tmp[:fact], file: file
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
require 'digest'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
#------------------------------------------------------------------------------
|
|
6
|
+
# cml 命令实现(CasetDown::Live)
|
|
7
|
+
#------------------------------------------------------------------------------
|
|
8
|
+
# Markdown 文件监听渲染:持续监视指定目录下的 .md 文档,
|
|
9
|
+
# 内容发生变更时自动执行 cm 默认渲染流程(表格 + 代码 + 渲染文件),
|
|
10
|
+
# 并把最新渲染版本同步为 "<文件名>.latest.md"。
|
|
11
|
+
#
|
|
12
|
+
# 命令行用法(bin/cml 薄启动器调用 CasetDown::Live.run):
|
|
13
|
+
#
|
|
14
|
+
# cml # 监视当前目录所有 .md
|
|
15
|
+
# cml <dir> # 监视指定目录
|
|
16
|
+
# cml <dir> <glob> # 监视指定目录下匹配 glob 的文档(如 '*.md')
|
|
17
|
+
#
|
|
18
|
+
# 变更检测:mtime 触发 + MD5 摘要确认(原版为 atime 触发,
|
|
19
|
+
# Windows 卷常见关闭 atime 更新会导致漏检,故改用 mtime)。
|
|
20
|
+
#------------------------------------------------------------------------------
|
|
21
|
+
module CasetDown
|
|
22
|
+
module Live
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# 命令行入口:解析参数并进入监听循环(正常情况下不返回)。
|
|
26
|
+
#
|
|
27
|
+
# @param argv [Array<String>] 命令行参数
|
|
28
|
+
# @return [Integer] 进程退出码(仅在参数校验失败时返回)
|
|
29
|
+
def run argv
|
|
30
|
+
if argv.empty?
|
|
31
|
+
cd = Dir.pwd
|
|
32
|
+
fs = '*'
|
|
33
|
+
else
|
|
34
|
+
cd = argv.first
|
|
35
|
+
fs = argv.size>1 ? argv.last : '*'
|
|
36
|
+
if Dir["#{cd}/#{fs}"].select{|p|p[-3..-1]=='.md'}.empty?
|
|
37
|
+
warn "No file(#{cd}/#{fs}) exist."
|
|
38
|
+
return 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
warn "DONT USE POSTFIX .latest.md, FILENAME.latest.md WONT BE UPDATED!"
|
|
43
|
+
|
|
44
|
+
table = {}
|
|
45
|
+
loop do
|
|
46
|
+
scan_once table, cd, fs
|
|
47
|
+
sleep 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 执行一轮扫描:登记新文件、检测内容变更并渲染。
|
|
52
|
+
#
|
|
53
|
+
# 供监听循环调用,独立暴露以便测试。
|
|
54
|
+
#
|
|
55
|
+
# @param table [Hash{String => Array}] 变更登记表
|
|
56
|
+
# 值为 [mtime, size, md5摘要] 三元组
|
|
57
|
+
# @param cd [String] 监视目录
|
|
58
|
+
# @param fs [String] 文件名匹配模式(glob)
|
|
59
|
+
# @return [Array<String>] 本轮发生变更并完成渲染的文件路径列表
|
|
60
|
+
def scan_once table, cd, fs
|
|
61
|
+
events = []
|
|
62
|
+
Dir["#{cd}/#{fs}"].each do|path|
|
|
63
|
+
next unless path[-3..-1]=='.md'
|
|
64
|
+
next if path[-10..-1]=='.latest.md'
|
|
65
|
+
unless table[path]
|
|
66
|
+
table[path] = [File.stat(path).mtime, File.stat(path).size, Digest::MD5.hexdigest(File.read(path))]
|
|
67
|
+
end
|
|
68
|
+
otime, osize, odigest = table[path]
|
|
69
|
+
mtime = File.stat(path).mtime
|
|
70
|
+
msize = File.stat(path).size
|
|
71
|
+
if mtime>otime # && (msize != osize || msize == osize)
|
|
72
|
+
adigest = Digest::MD5.hexdigest File.read(path)
|
|
73
|
+
if adigest!= odigest
|
|
74
|
+
text = render path
|
|
75
|
+
table[path] = [mtime, msize, adigest]
|
|
76
|
+
latpath = (Dir["#{path[0..-4]}.*.md"] - ["#{path[0..-4]}.latest.md"]).max
|
|
77
|
+
latest = "#{path[0..-4]}.latest.md"
|
|
78
|
+
File.delete(latest) if File.exist?(latest)
|
|
79
|
+
puts ">#{path} changed -> #{latpath || latest}(#{adigest}).";$stdout.flush
|
|
80
|
+
if latpath
|
|
81
|
+
FileUtils.copy_file(latpath, latest)
|
|
82
|
+
else
|
|
83
|
+
File.write(latest, text) # 兜底:无时间戳渲染文件时直接写入渲染文本
|
|
84
|
+
end
|
|
85
|
+
events << path
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
return events
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 渲染单个文档(等价于对该文件执行一次无参数 cm)。
|
|
93
|
+
#
|
|
94
|
+
# @param path [String] Markdown 文档路径
|
|
95
|
+
# @return [String, nil] 渲染回写后的完整文档文本
|
|
96
|
+
def render path
|
|
97
|
+
cds = CasetDown.load path
|
|
98
|
+
CasetDown::CLI.render_file cds
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
|
|
3
|
+
##################################################################################################################
|
|
4
|
+
#
|
|
5
|
+
# EnData::ApiRequest —— WEBOOT API 调用应用层
|
|
6
|
+
# 移植自 imdoc/EnData/api-app(依赖外部 gem webot 提供的 weboot 接口)。
|
|
7
|
+
#
|
|
8
|
+
# 本文件为可选加载:casetdown 入口在 require 时做了 LoadError 兜底,
|
|
9
|
+
# 未安装 webot 的环境中 cm/cml 与数据绑定功能不受影响,
|
|
10
|
+
# 仅 EnData::ApiRequest API 调用扩展不可用。
|
|
11
|
+
#
|
|
12
|
+
# API: template1 # an example of templates
|
|
13
|
+
# API: bind # single bind
|
|
14
|
+
# API: send # single send
|
|
15
|
+
# API: make # batch bind
|
|
16
|
+
# API: handle # batch send
|
|
17
|
+
# API: run # make + handle
|
|
18
|
+
#
|
|
19
|
+
# Step1: EnData::ApiRequest.make
|
|
20
|
+
# CASE: table1 × table2 × ... >--(join)--> task_table + bind_table(ref,val) >--eval(binding)--> task_instances
|
|
21
|
+
# USE: EnData.join_table() |> EnData.make_ref |> EnData.make_doc |> EnData::ApiRequest.bind
|
|
22
|
+
#
|
|
23
|
+
# Step2: EnData::ApiRequest.handle
|
|
24
|
+
# CASE: task_instances >--(API CALLING)--> running_results
|
|
25
|
+
# USE: EnData::ApiRequest.send
|
|
26
|
+
#
|
|
27
|
+
##################################################################################################################
|
|
28
|
+
require 'weboot'
|
|
29
|
+
|
|
30
|
+
# 便捷方法:把表名对应的数据体用分号拼接(API 命令序列)。
|
|
31
|
+
#
|
|
32
|
+
# @param tabname [String] 表名
|
|
33
|
+
# @return [String] 分号拼接的命令串
|
|
34
|
+
def cmds tabname
|
|
35
|
+
return EnData.get_body(tabname).join(";")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 便捷方法:批量生成并执行 API 任务(make + handle)。
|
|
39
|
+
#
|
|
40
|
+
# @param tables [Array<Array>] 参与拼接的任务表
|
|
41
|
+
# @param bind_table [Hash{String => String}] 引用名 => 引用文本
|
|
42
|
+
# @return [Array] 执行结果列表
|
|
43
|
+
def run tables, bind_table
|
|
44
|
+
EnData::ApiRequest.run tables, bind_table
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module EnData
|
|
48
|
+
# API 请求应用层。
|
|
49
|
+
module ApiRequest
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
# 示例请求模板(bind 的缺省模板)。
|
|
53
|
+
#
|
|
54
|
+
# @return [String] 请求模板文本
|
|
55
|
+
def template1
|
|
56
|
+
%Q|POST ${path} HTTP/1.1
|
|
57
|
+
Host: ${server}
|
|
58
|
+
Accept: */*
|
|
59
|
+
Content-Type: application/json
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
"protocol": "${protocol}",
|
|
63
|
+
"host": "${device}",
|
|
64
|
+
"user": "${access-key}",
|
|
65
|
+
"app-key": "${app-key}",
|
|
66
|
+
"cmds": "${operations}",
|
|
67
|
+
"method": "${processes}"
|
|
68
|
+
}|
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# 把单条绑定数据填入请求模板。
|
|
72
|
+
#
|
|
73
|
+
# @param options [Hash{String => Object}] 模板字段取值
|
|
74
|
+
# @param template [String] 请求模板,缺省 template1
|
|
75
|
+
# @return [String] 填充后的请求文本
|
|
76
|
+
def bind options, template=EnData::ApiRequest.template1
|
|
77
|
+
Replacement.init
|
|
78
|
+
['path','server','protocol','device','access-key','app-key','operations','processes'].each do|key|
|
|
79
|
+
Replacement[key] = options[key].to_s
|
|
80
|
+
end
|
|
81
|
+
return TinText.instance(template)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 发送单条请求并解析响应。
|
|
85
|
+
#
|
|
86
|
+
# @param context [String] 请求文本
|
|
87
|
+
# @return [Object] 响应中的 content 字段
|
|
88
|
+
def send context
|
|
89
|
+
request = Weboot.request context
|
|
90
|
+
response = Weboot.construct request
|
|
91
|
+
report = JSON.parse(response['body'])['content']
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 批量绑定:拼接任务表、替换引用、生成任务实例。
|
|
95
|
+
#
|
|
96
|
+
# @param tables [Array<Array>] 任务表列表(依次笛卡尔拼接)
|
|
97
|
+
# @param bind_table [Hash{String => String}] 引用名 => 引用文本
|
|
98
|
+
# @return [Array<String>] 任务实例列表
|
|
99
|
+
def make tables, bind_table
|
|
100
|
+
# Please don't join too much tables
|
|
101
|
+
tasks = tables.shift
|
|
102
|
+
tasks = EnData.join_table(tasks, tables.shift) until tables.empty?
|
|
103
|
+
bind_table.each{|refname, reftext|tasks = EnData.make_ref(tasks, refname, reftext)}
|
|
104
|
+
binds = EnData.make_doc(tasks)
|
|
105
|
+
instances = binds.map{|binding|EnData::ApiRequest.bind binding} # with EnData::ApiRequest::template
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 批量发送:逐条执行任务实例。
|
|
109
|
+
#
|
|
110
|
+
# @param instances [Array<String>] 任务实例列表
|
|
111
|
+
# @return [Array] 执行结果列表
|
|
112
|
+
def handle instances
|
|
113
|
+
instances.map{|instance| EnData::ApiRequest.send instance}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# make + handle 一体化执行。
|
|
117
|
+
#
|
|
118
|
+
# @param tables [Array<Array>] 任务表列表
|
|
119
|
+
# @param bind_table [Hash{String => String}] 引用名 => 引用文本
|
|
120
|
+
# @return [Array] 执行结果列表
|
|
121
|
+
def run tables, bind_table
|
|
122
|
+
instances = EnData::ApiRequest.make tables, bind_table
|
|
123
|
+
EnData::ApiRequest.handle(instances)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#coding:utf-8
|
|
2
|
+
|
|
3
|
+
['yaml','json'].each{|mod|require mod}
|
|
4
|
+
|
|
5
|
+
#------------------------------------------------------------------------------
|
|
6
|
+
# 脚本内嵌数据整理(EnData)
|
|
7
|
+
#------------------------------------------------------------------------------
|
|
8
|
+
# 移植自 imdoc/EnData/endata + endata-app,为 casetdown 的代码执行通道提供
|
|
9
|
+
# 数据绑定能力:表格与全局配置以 endata 格式附着在脚本 __END__ 之后,
|
|
10
|
+
# 执行中的脚本通过 EnData.table('表名') 取回二维表数据。
|
|
11
|
+
#
|
|
12
|
+
# endata 数据段格式(位于脚本 __END__ 之后,有顺序要求):
|
|
13
|
+
#
|
|
14
|
+
# [global@endata ~]$ruby global
|
|
15
|
+
# {"配置名"=>"配置值", ...}
|
|
16
|
+
#
|
|
17
|
+
# [table@endata ~]$ruby 表名
|
|
18
|
+
# [["字段1", "字段2"], ["值1", "值2"], ...]
|
|
19
|
+
#
|
|
20
|
+
# 行格式为 [用户@endata 路径]$解释器 名称 参数1# ...
|
|
21
|
+
# - 用户/路径:占位语义,可自由命名
|
|
22
|
+
# - 解释器:ruby / yaml / json / plaintext,决定 source 的解析方式
|
|
23
|
+
# - 名称:表的唯一标识(EnData.table 的检索键)
|
|
24
|
+
#
|
|
25
|
+
# 使用样例见 doc/MANUAL.md 第 5 节“数据绑定”。
|
|
26
|
+
#------------------------------------------------------------------------------
|
|
27
|
+
module EnData
|
|
28
|
+
extend self
|
|
29
|
+
|
|
30
|
+
# 读取当前脚本的 __END__ 数据段(DATA 常量),并做结果缓存。
|
|
31
|
+
#
|
|
32
|
+
# @note 仅在带 __END__ 段的脚本(cm 生成的执行脚本)中可用
|
|
33
|
+
# @return [String] 数据段原始文本
|
|
34
|
+
def load
|
|
35
|
+
@__EnData__ ||= DATA.read
|
|
36
|
+
return @__EnData__
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 解析 endata 数据段为 {名称 => 数据项} 哈希。
|
|
40
|
+
#
|
|
41
|
+
# 每个数据项包含:user/path/handler/name/arguments/context 七个键。
|
|
42
|
+
#
|
|
43
|
+
# @param data [String, nil] 数据段文本,缺省读取当前脚本 DATA
|
|
44
|
+
# @return [Hash{String => Hash}] 数据项列表(按名称索引)
|
|
45
|
+
# ['username'@EnData 'workpath']$ 'command' 'scriptname' 'argument1#' ...
|
|
46
|
+
def parse data=@__EnData__
|
|
47
|
+
@__datalist__ = {}
|
|
48
|
+
temp = {
|
|
49
|
+
'user' => '',
|
|
50
|
+
'path' => '',
|
|
51
|
+
'handler' => '',
|
|
52
|
+
'name' => '',
|
|
53
|
+
'arguments' => [],
|
|
54
|
+
'context' => []
|
|
55
|
+
}
|
|
56
|
+
data.split("\n").each do|line|
|
|
57
|
+
if matcher = /^\[[\w|\s]*\@endata[\w|\s|\/|\~]*\]\$/.match(line)
|
|
58
|
+
@__datalist__[temp['name']] = temp
|
|
59
|
+
front, back = line.split("@endata")
|
|
60
|
+
usr = front.sub("[",'').strip
|
|
61
|
+
pwd = back.split("]")[0].strip
|
|
62
|
+
cmd = back.split("]$")[-1].split(" ")
|
|
63
|
+
handler, name, arguments = cmd[0], cmd[1], cmd[2..-1]
|
|
64
|
+
context = ''
|
|
65
|
+
temp = {
|
|
66
|
+
'user' => usr,
|
|
67
|
+
'path' => pwd,
|
|
68
|
+
'handler' => handler,
|
|
69
|
+
'name' => name,
|
|
70
|
+
'arguments' => arguments,
|
|
71
|
+
'context' => []
|
|
72
|
+
}
|
|
73
|
+
else
|
|
74
|
+
temp['context'] << line
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
@__datalist__[temp['name']] = temp
|
|
78
|
+
return @__datalist__
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 按名称或属性选择数据项。
|
|
82
|
+
#
|
|
83
|
+
# @param options [Hash] 检索条件
|
|
84
|
+
# - :name 按名称精确选择
|
|
85
|
+
# - 其他键按数据项属性过滤(user/path/handler/...)
|
|
86
|
+
# @return [Hash, nil] 命中的数据项
|
|
87
|
+
def select options={}
|
|
88
|
+
if name = options[:name]
|
|
89
|
+
@__datalist__[name]
|
|
90
|
+
else
|
|
91
|
+
sets = @__datalist__.values
|
|
92
|
+
options.each do|key, val|
|
|
93
|
+
sets = sets.select{|s|s[key.to_s]==val.to_s}
|
|
94
|
+
end
|
|
95
|
+
sets.first
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 把数据项正文按解释器还原为 ruby / yaml / json / plaintext 对象。
|
|
100
|
+
#
|
|
101
|
+
# @param script [Hash, nil] parse 产生的数据项
|
|
102
|
+
# @return [Object, nil] 解析结果
|
|
103
|
+
def source script
|
|
104
|
+
return nil unless script && script['context']
|
|
105
|
+
context = script['context'].join("\n")
|
|
106
|
+
case script['handler']
|
|
107
|
+
when 'ruby'
|
|
108
|
+
context
|
|
109
|
+
when 'yaml'
|
|
110
|
+
YAML.load context
|
|
111
|
+
when 'json'
|
|
112
|
+
JSON.parse context
|
|
113
|
+
else # plaintext
|
|
114
|
+
context
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ruby inline:把数据段中的 ruby 文本在模块上下文中执行。
|
|
119
|
+
#
|
|
120
|
+
# @param scripts [String, nil] ruby 源码文本
|
|
121
|
+
# @yield [void] 无 scripts 时执行 block
|
|
122
|
+
# @return [Object] 执行结果
|
|
123
|
+
def run scripts=nil, &script_block
|
|
124
|
+
scripts ? self.module_eval(scripts) : self.module_eval(&script_block)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
#------------------------------------------------------------------------------
|
|
129
|
+
# EnData 应用层(移植自 imdoc/EnData/endata-app)
|
|
130
|
+
#------------------------------------------------------------------------------
|
|
131
|
+
module EnData
|
|
132
|
+
module_function
|
|
133
|
+
|
|
134
|
+
# 取回指定名称的表格数据(执行脚本内最常用的入口)。
|
|
135
|
+
#
|
|
136
|
+
# [CUSTOM_YOUR_NAME@endata CUSTOM_YOUR_PATH]$ruby table
|
|
137
|
+
#
|
|
138
|
+
# @param name [String] 表名,缺省 'table';全局配置固定占用表名 'global'
|
|
139
|
+
# @return [Object] 表格数据(通常是二维数组)
|
|
140
|
+
def table name='table'
|
|
141
|
+
EnData.load()
|
|
142
|
+
EnData.parse()
|
|
143
|
+
EnData.run(EnData.source EnData.select name: name)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# 表1 × 表2 笛卡尔拼接(行对行叉积)。
|
|
147
|
+
#
|
|
148
|
+
# @param name1 [String] 左表名
|
|
149
|
+
# @param name2 [String] 右表名
|
|
150
|
+
# @return [Array<Array>] 拼接结果新表
|
|
151
|
+
def join_table name1, name2
|
|
152
|
+
table1 = EnData.table(name1)
|
|
153
|
+
table2 = EnData.table(name2)
|
|
154
|
+
table1.绑定表头 table1.first
|
|
155
|
+
table2.绑定表头 table2.first
|
|
156
|
+
return table1.拼接 table2
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 取表格的数据体(去表头)。
|
|
160
|
+
#
|
|
161
|
+
# @param name [String] 表名
|
|
162
|
+
# @return [Array<Array>] 数据行列表
|
|
163
|
+
def get_body name
|
|
164
|
+
EnData.table(name)[1..-1]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# 把 table[head, record...] 档案化为 {字段=>值} 对象数组。
|
|
168
|
+
#
|
|
169
|
+
# CASE: table[head=[field*], record=[value*]] >----> tree{field=>value, ...}
|
|
170
|
+
#
|
|
171
|
+
# @param table [Array<Array>] 二维表
|
|
172
|
+
# @return [Array<Hash>] 对象数组
|
|
173
|
+
def make_doc table
|
|
174
|
+
return table[1..-1].inject([]) do|doc, record|
|
|
175
|
+
rec = {}
|
|
176
|
+
record.each_with_index{|value, index|rec[table[0][index]] = value}
|
|
177
|
+
doc.push rec; doc
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# 把表中 ${引用名} 占位替换为引用文本,生成新表。
|
|
182
|
+
#
|
|
183
|
+
# CASE: ${refname} >--(replace)--> reftext
|
|
184
|
+
#
|
|
185
|
+
# @param table [Array<Array>] 二维表
|
|
186
|
+
# @param refname [String] 引用名
|
|
187
|
+
# @param reftext [String] 引用文本
|
|
188
|
+
# @return [Array<Array>] 替换结果新表
|
|
189
|
+
def make_ref table, refname, reftext
|
|
190
|
+
return table.inject([]) do|newtab, record|
|
|
191
|
+
newrec = []
|
|
192
|
+
record.each{|field|newrec << (field=="${#{refname}}" ? reftext : field)}
|
|
193
|
+
newtab.push newrec; newtab
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
end
|