aspen-cli 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 +7 -0
- data/.github/FUNDING.yml +12 -0
- data/.gitignore +16 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +21 -0
- data/README.md +84 -0
- data/Rakefile +10 -0
- data/aspen.gemspec +48 -0
- data/bin/aspen +3 -0
- data/bin/console +22 -0
- data/bin/setup +8 -0
- data/lib/aspen/abstract_parser.rb +72 -0
- data/lib/aspen/abstract_statement.rb +34 -0
- data/lib/aspen/actions/compile.rb +31 -0
- data/lib/aspen/actions/push.rb +39 -0
- data/lib/aspen/actions/watch.rb +55 -0
- data/lib/aspen/actions.rb +8 -0
- data/lib/aspen/adapters.rb +38 -0
- data/lib/aspen/ast/nodes/attribute.rb +16 -0
- data/lib/aspen/ast/nodes/comment.rb +15 -0
- data/lib/aspen/ast/nodes/content.rb +17 -0
- data/lib/aspen/ast/nodes/custom_statement.rb +19 -0
- data/lib/aspen/ast/nodes/edge.rb +15 -0
- data/lib/aspen/ast/nodes/label.rb +15 -0
- data/lib/aspen/ast/nodes/narrative.rb +15 -0
- data/lib/aspen/ast/nodes/node.rb +20 -0
- data/lib/aspen/ast/nodes/statement.rb +17 -0
- data/lib/aspen/ast/nodes/type.rb +46 -0
- data/lib/aspen/ast.rb +18 -0
- data/lib/aspen/cli/commands/build.rb +26 -0
- data/lib/aspen/cli/commands/build_steps.rb +204 -0
- data/lib/aspen/cli/commands/compile.rb +27 -0
- data/lib/aspen/cli/commands/generate.rb +23 -0
- data/lib/aspen/cli/commands/new.rb +115 -0
- data/lib/aspen/cli/commands/push.rb +15 -0
- data/lib/aspen/cli/commands/version.rb +15 -0
- data/lib/aspen/cli/commands/watch.rb +30 -0
- data/lib/aspen/cli/commands.rb +28 -0
- data/lib/aspen/cli/templates/.gitignore +6 -0
- data/lib/aspen/cli/templates/airtable.yml +1 -0
- data/lib/aspen/cli/templates/convert +16 -0
- data/lib/aspen/cli/templates/db.yml.erb +22 -0
- data/lib/aspen/cli/templates/docker-compose.yml +23 -0
- data/lib/aspen/cli/templates/manifest.yml.erb +31 -0
- data/lib/aspen/cli.rb +9 -0
- data/lib/aspen/compiler.rb +209 -0
- data/lib/aspen/contracts/default_attribute_contract.rb +29 -0
- data/lib/aspen/contracts.rb +1 -0
- data/lib/aspen/conversion.rb +43 -0
- data/lib/aspen/custom_grammar/ast/nodes/bare.rb +17 -0
- data/lib/aspen/custom_grammar/ast/nodes/capture_segment.rb +19 -0
- data/lib/aspen/custom_grammar/ast/nodes/content.rb +17 -0
- data/lib/aspen/custom_grammar/ast/nodes/expression.rb +17 -0
- data/lib/aspen/custom_grammar/ast.rb +13 -0
- data/lib/aspen/custom_grammar/compiler.rb +80 -0
- data/lib/aspen/custom_grammar/grammar.rb +78 -0
- data/lib/aspen/custom_grammar/lexer.rb +76 -0
- data/lib/aspen/custom_grammar/matcher.rb +43 -0
- data/lib/aspen/custom_grammar/parser.rb +51 -0
- data/lib/aspen/custom_grammar.rb +23 -0
- data/lib/aspen/custom_statement.rb +35 -0
- data/lib/aspen/discourse.rb +122 -0
- data/lib/aspen/edge.rb +35 -0
- data/lib/aspen/errors.rb +158 -0
- data/lib/aspen/helpers.rb +17 -0
- data/lib/aspen/lexer.rb +195 -0
- data/lib/aspen/list.rb +19 -0
- data/lib/aspen/node.rb +53 -0
- data/lib/aspen/parser.rb +183 -0
- data/lib/aspen/renderers/abstract_renderer.rb +22 -0
- data/lib/aspen/renderers/cypher_base_renderer.rb +36 -0
- data/lib/aspen/renderers/cypher_batch_renderer.rb +55 -0
- data/lib/aspen/renderers/cypher_renderer.rb +18 -0
- data/lib/aspen/renderers/gexf_renderer.rb +47 -0
- data/lib/aspen/renderers/json_renderer.rb +40 -0
- data/lib/aspen/renderers.rb +9 -0
- data/lib/aspen/schemas/discourse_schema.rb +64 -0
- data/lib/aspen/schemas/grammar_schema.rb +24 -0
- data/lib/aspen/statement.rb +42 -0
- data/lib/aspen/system_default.rb +12 -0
- data/lib/aspen/version.rb +3 -0
- data/lib/aspen.rb +65 -0
- metadata +300 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Aspen
|
|
2
|
+
module CustomGrammar
|
|
3
|
+
module AST
|
|
4
|
+
module Nodes
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require 'aspen/custom_grammar/ast/nodes/bare'
|
|
11
|
+
require 'aspen/custom_grammar/ast/nodes/capture_segment'
|
|
12
|
+
require 'aspen/custom_grammar/ast/nodes/content'
|
|
13
|
+
require 'aspen/custom_grammar/ast/nodes/expression'
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module Aspen
|
|
2
|
+
module CustomGrammar
|
|
3
|
+
class Compiler
|
|
4
|
+
|
|
5
|
+
attr_reader :root, :environment
|
|
6
|
+
|
|
7
|
+
def self.render(root, environment = {})
|
|
8
|
+
new(root, environment).render
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.compile(root, environment = {})
|
|
12
|
+
new(root, environment).compile
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(root, environment = {})
|
|
16
|
+
@root = root
|
|
17
|
+
@environment = environment
|
|
18
|
+
@type_registry = {}
|
|
19
|
+
@label_registry = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def compile
|
|
23
|
+
# Call #render before accessing the registry. If this code
|
|
24
|
+
# changes, we may need more process control to ensure #render
|
|
25
|
+
# goes first.
|
|
26
|
+
{
|
|
27
|
+
pattern: render,
|
|
28
|
+
type_registry: @type_registry,
|
|
29
|
+
label_registry: @label_registry
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def render
|
|
34
|
+
visit(root)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def visit(node)
|
|
38
|
+
short_name = node.class.to_s.split('::').last.downcase
|
|
39
|
+
method_name = "visit_#{short_name}"
|
|
40
|
+
# puts "---- #{method_name}"
|
|
41
|
+
# puts node.inspect
|
|
42
|
+
send(method_name, node)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def visit_expression(node)
|
|
46
|
+
segments = node.segments.map { |segment| visit(segment) }
|
|
47
|
+
segments.last.gsub!(/\.$/, '') # Make the last period optional? Maybe?
|
|
48
|
+
segments.unshift "^" # Add a bol matcher to the front.
|
|
49
|
+
segments.push "\\.?$" # Make the last period optional? Again?
|
|
50
|
+
Regexp.new(segments.join)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def visit_capturesegment(node)
|
|
54
|
+
value = case node.type
|
|
55
|
+
when :integer then /(?<#{node.var_name}>[\d,]+\d*)/ # No decimal
|
|
56
|
+
when :float then /(?<#{node.var_name}>[\d,]+\.\d+)/ # Decimal point required
|
|
57
|
+
when :numeric then /(?<#{node.var_name}>[\d,]+\.?\d*)/ # Optional decimal
|
|
58
|
+
when :string then /(?<#{node.var_name}>.*?)/
|
|
59
|
+
when :node then /(?<#{node.var_name}>.*?)/
|
|
60
|
+
else
|
|
61
|
+
raise ArgumentError, "No regexp pattern for type \"#{node.type}\"."
|
|
62
|
+
end
|
|
63
|
+
# Add type to type registry
|
|
64
|
+
@type_registry[node.var_name] = node.type
|
|
65
|
+
# Add label to label registry
|
|
66
|
+
@label_registry[node.var_name] = node.label if node.type == :node
|
|
67
|
+
return value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_bare(node)
|
|
71
|
+
visit(node.content)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def visit_content(node)
|
|
75
|
+
node.content
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module Aspen
|
|
2
|
+
module CustomGrammar
|
|
3
|
+
class Grammar
|
|
4
|
+
|
|
5
|
+
# API Surface:
|
|
6
|
+
# #add?
|
|
7
|
+
# #match? (Boolean) -> Does the grammar cover this string, match this case?
|
|
8
|
+
# #match (Matcher) -> Request the matcher for this string.
|
|
9
|
+
# #compile_pattern (Regexp) -> Given a matcher expression, return the Regexp pattern that can match against a string.
|
|
10
|
+
|
|
11
|
+
attr_reader :registry
|
|
12
|
+
|
|
13
|
+
include Dry::Monads[:maybe]
|
|
14
|
+
|
|
15
|
+
def initialize()
|
|
16
|
+
@registry = []
|
|
17
|
+
@slug_counters = Hash.new { 1 }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Does the given text match a matcher?
|
|
21
|
+
def match?(string)
|
|
22
|
+
!!match(string)
|
|
23
|
+
rescue Aspen::Error
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match(text)
|
|
28
|
+
results = @registry.select { |m| m.match?(text) }
|
|
29
|
+
warn "Found #{results.count} matches" if results.count > 1
|
|
30
|
+
# raise Aspen::Error, "No results." if results.empty?
|
|
31
|
+
return results.first
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias_method :matcher_for, :match
|
|
35
|
+
|
|
36
|
+
def inspect
|
|
37
|
+
"#<Aspen::Grammar matchers: #{count}>"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def count
|
|
41
|
+
registry.count
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def add(maybe_matchers)
|
|
45
|
+
matchers = Array(maybe_matchers).flatten
|
|
46
|
+
raise unless matchers.all? { |m| m.is_a? Aspen::CustomGrammar::Matcher }
|
|
47
|
+
matchers.each { |matcher| @registry << matcher }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# This doesn't quite work, because var results is untyped.
|
|
51
|
+
def render(content)
|
|
52
|
+
matcher = matcher_for(content)
|
|
53
|
+
results = results_for(content)
|
|
54
|
+
template = matcher.template
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def results_for(text)
|
|
58
|
+
matcher_for(text).captures!(text)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def match!(text)
|
|
62
|
+
unless match(text)
|
|
63
|
+
raise Aspen::MatchError, <<~ERROR
|
|
64
|
+
Couldn't find an Aspen grammar that matched the line:
|
|
65
|
+
|
|
66
|
+
#{text}
|
|
67
|
+
|
|
68
|
+
For more details (if you can), try running this to see all the match patterns:
|
|
69
|
+
|
|
70
|
+
Aspen::Grammar.registry.map(&:pattern)
|
|
71
|
+
|
|
72
|
+
ERROR
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require 'strscan'
|
|
2
|
+
|
|
3
|
+
module Aspen
|
|
4
|
+
module CustomGrammar
|
|
5
|
+
class Lexer
|
|
6
|
+
def self.tokenize(code, env={})
|
|
7
|
+
new.tokenize(code, env)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def tokenize(code, env={})
|
|
11
|
+
scanner = StringScanner.new(code)
|
|
12
|
+
tokens = []
|
|
13
|
+
|
|
14
|
+
# puts "tokens: #{tokens} | state: #{state} | stack: #{stack}"
|
|
15
|
+
|
|
16
|
+
until scanner.eos?
|
|
17
|
+
case state
|
|
18
|
+
when :default then
|
|
19
|
+
if scanner.scan(/\(/)
|
|
20
|
+
tokens << [:OPEN_PARENS]
|
|
21
|
+
push_state :capture_segment
|
|
22
|
+
elsif scanner.scan(/[[[:alnum:]][[:blank:]]\!"\#$%&'*+,\-.\/:;<=>?@\[\\\]^_‘\{\|\}~]+/)
|
|
23
|
+
tokens << [:BARE, scanner.matched]
|
|
24
|
+
else
|
|
25
|
+
no_match(scanner, state)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
when :capture_segment
|
|
29
|
+
if scanner.scan(/\s+/)
|
|
30
|
+
# NO OP
|
|
31
|
+
elsif scanner.scan(/^(numeric|integer|float|string)/)
|
|
32
|
+
tokens << [:TYPE, scanner.matched]
|
|
33
|
+
elsif scanner.scan(Aspen::Lexer::PASCAL_CASE)
|
|
34
|
+
tokens << [:TYPE, ["node", scanner.matched]]
|
|
35
|
+
# TODO: This should only accept legal variable names, like `hello_01`
|
|
36
|
+
elsif scanner.scan(/^\w+/)
|
|
37
|
+
tokens << [:VAR_NAME, scanner.matched]
|
|
38
|
+
elsif scanner.scan(/\)/)
|
|
39
|
+
tokens << [:CLOSE_PARENS]
|
|
40
|
+
pop_state
|
|
41
|
+
else
|
|
42
|
+
no_match(scanner, state)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
else
|
|
46
|
+
raise Aspen::LexError, "There is no matcher for state #{state.inspect}."
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
tokens
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def stack
|
|
54
|
+
@stack ||= []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def state
|
|
58
|
+
stack.last || :default
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def push_state(state)
|
|
62
|
+
stack.push(state)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def pop_state
|
|
66
|
+
stack.pop
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def no_match(scanner, state)
|
|
70
|
+
raise Aspen::LexError,
|
|
71
|
+
Aspen::Errors.messages(:unexpected_token, scanner, state)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'mustache'
|
|
2
|
+
|
|
3
|
+
module Aspen
|
|
4
|
+
module CustomGrammar
|
|
5
|
+
class Matcher
|
|
6
|
+
|
|
7
|
+
attr_accessor :expression, :template, :pattern, :typereg, :labelreg
|
|
8
|
+
|
|
9
|
+
def initialize(expression: , template: , pattern: )
|
|
10
|
+
@expression = expression
|
|
11
|
+
@template = template
|
|
12
|
+
# SMELL: I don't like this design.
|
|
13
|
+
compiled_grammar = Aspen::CustomGrammar.compile(expression)
|
|
14
|
+
@pattern = compiled_grammar[:pattern]
|
|
15
|
+
@typereg = compiled_grammar[:type_registry]
|
|
16
|
+
@labelreg = compiled_grammar[:label_registry]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def match?(str)
|
|
20
|
+
pattern.match?(str)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Compare against narrative line to get captures
|
|
24
|
+
# Example results: { a: , amt: , b: }
|
|
25
|
+
def captures(str)
|
|
26
|
+
pattern.match(str).named_captures
|
|
27
|
+
end
|
|
28
|
+
alias_method :results, :captures
|
|
29
|
+
|
|
30
|
+
def captures!(str)
|
|
31
|
+
unless match?(str)
|
|
32
|
+
raise Aspen::MatchError,
|
|
33
|
+
Aspen::Errors.messages(:no_grammar_match, pattern, str)
|
|
34
|
+
end
|
|
35
|
+
captures(str)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias_method :results!, :captures!
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module Aspen
|
|
2
|
+
module CustomGrammar
|
|
3
|
+
class Parser < Aspen::AbstractParser
|
|
4
|
+
|
|
5
|
+
# expression = { segment }
|
|
6
|
+
# segment = BARE || capture_segment
|
|
7
|
+
# capture_segment = OPEN_PARENS, type, VAR_NAME, CLOSE_PARENS
|
|
8
|
+
# type = { node CONTENT } || numeric | float | integer | string
|
|
9
|
+
|
|
10
|
+
def parse
|
|
11
|
+
Aspen::CustomGrammar::AST::Nodes::Expression.new(parse_expression)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def parse_expression
|
|
15
|
+
segments = []
|
|
16
|
+
|
|
17
|
+
# Make sure this returns on empty
|
|
18
|
+
while segment = parse_segment
|
|
19
|
+
segments << segment
|
|
20
|
+
break if tokens[position].nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
segments
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_segment
|
|
27
|
+
return parse_bare_segment || parse_capture_segment
|
|
28
|
+
raise Aspen::ParseError, "Didn't match expected tokens, got\n\t#{upcoming.inspect}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse_bare_segment
|
|
32
|
+
if content = expect(:BARE)
|
|
33
|
+
Aspen::CustomGrammar::AST::Nodes::Bare.new(content.first.last)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse_capture_segment
|
|
38
|
+
if (_, type, var_name, _ = expect(:OPEN_PARENS, :TYPE, :VAR_NAME, :CLOSE_PARENS))
|
|
39
|
+
type_name, label = type.last
|
|
40
|
+
|
|
41
|
+
Aspen::CustomGrammar::AST::Nodes::CaptureSegment.new(
|
|
42
|
+
type: type_name.to_sym,
|
|
43
|
+
var_name: var_name.last,
|
|
44
|
+
label: label
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require 'aspen/custom_grammar/ast'
|
|
2
|
+
require 'aspen/custom_grammar/lexer'
|
|
3
|
+
require 'aspen/custom_grammar/parser'
|
|
4
|
+
require 'aspen/custom_grammar/compiler'
|
|
5
|
+
|
|
6
|
+
require 'aspen/custom_grammar/matcher'
|
|
7
|
+
require 'aspen/custom_grammar/grammar'
|
|
8
|
+
|
|
9
|
+
module Aspen
|
|
10
|
+
module CustomGrammar
|
|
11
|
+
|
|
12
|
+
def self.compile(expression)
|
|
13
|
+
tokens = Aspen::CustomGrammar::Lexer.tokenize(expression)
|
|
14
|
+
ast = Aspen::CustomGrammar::Parser.parse(tokens)
|
|
15
|
+
Aspen::CustomGrammar::Compiler.compile(ast)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.compile_pattern(expression)
|
|
19
|
+
self.compile(expression)[:pattern]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'dry/types'
|
|
2
|
+
|
|
3
|
+
module Aspen
|
|
4
|
+
class CustomStatement < AbstractStatement
|
|
5
|
+
|
|
6
|
+
include Dry::Monads[:maybe]
|
|
7
|
+
|
|
8
|
+
attr_reader :nodes, :type
|
|
9
|
+
|
|
10
|
+
def type
|
|
11
|
+
:custom
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @todo The signature of the custom statement should be the
|
|
15
|
+
# Cypher template for that statement.
|
|
16
|
+
def signature
|
|
17
|
+
"custom"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param nodes [Array<Aspen::Node>] a list of nodes
|
|
21
|
+
# @param cypher [String] the Cypher generated by the template
|
|
22
|
+
# by the compiler
|
|
23
|
+
# @todo The inclusion of the Cypher parameter *suggests* that
|
|
24
|
+
# there might be a better way to generate the Cypher from a
|
|
25
|
+
# custom statement. Not sure.
|
|
26
|
+
def initialize(nodes: , cypher: )
|
|
27
|
+
@nodes = nodes
|
|
28
|
+
@cypher = cypher
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_cypher
|
|
32
|
+
@cypher
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
|
2
|
+
require 'dry/monads'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
require 'aspen/schemas/discourse_schema'
|
|
6
|
+
|
|
7
|
+
module Aspen
|
|
8
|
+
class Discourse
|
|
9
|
+
|
|
10
|
+
include Dry::Monads[:maybe]
|
|
11
|
+
|
|
12
|
+
attr_reader :data, :grammar
|
|
13
|
+
|
|
14
|
+
def self.from_yaml(yaml)
|
|
15
|
+
from_hash YAML.load(yaml)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.from_hash(data = {})
|
|
19
|
+
raise ArgumentError, "Must be a hash, was a #{data.class}" unless data.is_a?(Hash)
|
|
20
|
+
result = Schemas::DiscourseSchema.call(data)
|
|
21
|
+
if result.success?
|
|
22
|
+
new(data)
|
|
23
|
+
else
|
|
24
|
+
# TODO: Improve this output for human readability
|
|
25
|
+
raise Aspen::Error, result.errors.messages.to_s
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.assert(data)
|
|
30
|
+
return nil if data.nil?
|
|
31
|
+
case data.class.to_s
|
|
32
|
+
when "Aspen::Discourse" then data
|
|
33
|
+
when "Hash" then from_hash(data)
|
|
34
|
+
when "String" then from_yaml(data)
|
|
35
|
+
else
|
|
36
|
+
raise ArgumentError, "Must be a Hash, string (containing YAML), or Aspen::Discourse, got:\n\t#{data} (#{data.class})"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(data = {})
|
|
41
|
+
@data = data.with_indifferent_access
|
|
42
|
+
@grammar = Aspen::CustomGrammar::Grammar.new
|
|
43
|
+
process_grammar
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def default_label
|
|
47
|
+
maybe_label = Maybe(@data.dig(:default, :label))
|
|
48
|
+
maybe_label.value_or(Aspen::SystemDefault.label)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def default_attr_name(label)
|
|
52
|
+
maybe_attr = Maybe(@data.dig(:default, :attributes, label.to_sym))
|
|
53
|
+
maybe_attr.value_or(primary_default_attr_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def allowed_labels
|
|
57
|
+
@al ||= whitelist_for(:nodes)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def allowed_edges
|
|
61
|
+
@ae ||= whitelist_for(:edges)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def allows_label?(label)
|
|
65
|
+
allowed_labels.empty? || allowed_labels.include?(label)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def allows_edge?(edge)
|
|
69
|
+
allowed_edges.empty? || allowed_edges.include?(edge)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mutual
|
|
73
|
+
maybe_list = Maybe(@data.dig(:mutual) || @data.dig(:reciprocal))
|
|
74
|
+
maybe_list.value_or(Array.new)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def mutual?(edge_name)
|
|
78
|
+
mutual.include? edge_name
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
alias_method :reciprocal, :mutual
|
|
82
|
+
alias_method :reciprocal?, :mutual?
|
|
83
|
+
|
|
84
|
+
def add_grammar(grammar)
|
|
85
|
+
@grammar = grammar
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def primary_default_attr_name
|
|
91
|
+
maybe_attr = Maybe(@data.dig(:default, :attribute))
|
|
92
|
+
maybe_attr.value_or(Aspen::SystemDefault.attr_name)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def whitelist_for(stuff)
|
|
96
|
+
maybe_whitelist = Maybe(@data.dig(:allow_only, stuff))
|
|
97
|
+
list = maybe_whitelist.value_or([])
|
|
98
|
+
return list if list.is_a? Array # If it's already a YAML list, great.
|
|
99
|
+
list.split(",").map(&:strip) # Otherwise, split the comma-separated string
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Converts multiple lines
|
|
103
|
+
def process_grammar
|
|
104
|
+
return false unless configured_grammar
|
|
105
|
+
configured_grammar.each do |block|
|
|
106
|
+
Array(block.fetch(:match)).each do |expression|
|
|
107
|
+
matcher = Aspen::CustomGrammar::Matcher.new(
|
|
108
|
+
expression: expression,
|
|
109
|
+
template: block.fetch(:template),
|
|
110
|
+
pattern: Aspen::CustomGrammar.compile_pattern(expression)
|
|
111
|
+
)
|
|
112
|
+
grammar.add(matcher)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def configured_grammar
|
|
118
|
+
@cg ||= Maybe(@data.dig(:grammar)).value_or(false)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/aspen/edge.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Aspen
|
|
2
|
+
class Edge
|
|
3
|
+
|
|
4
|
+
# @todo Rename :word to :label
|
|
5
|
+
def initialize(name, mutual: false)
|
|
6
|
+
@name = name
|
|
7
|
+
@mutual = mutual
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def label
|
|
11
|
+
@name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_cypher
|
|
15
|
+
"-[:#{label.parameterize.underscore.upcase}]-#{cap}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def signature
|
|
19
|
+
to_cypher.gsub(/:/, '')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def mutual?
|
|
23
|
+
@mutual
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
alias_method :reciprocal?, :mutual?
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def cap
|
|
31
|
+
@mutual ? "" : ">"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
end
|