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,115 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module Aspen
|
|
5
|
+
module CLI
|
|
6
|
+
module Commands
|
|
7
|
+
|
|
8
|
+
class New < Dry::CLI::Command
|
|
9
|
+
desc "Generate a new Aspen project"
|
|
10
|
+
|
|
11
|
+
argument :project_name,
|
|
12
|
+
desc: "Name for new Aspen project",
|
|
13
|
+
required: true
|
|
14
|
+
|
|
15
|
+
option :database_url,
|
|
16
|
+
desc: "Database to push Aspen data to",
|
|
17
|
+
aliases: ["d"],
|
|
18
|
+
default: "http://neo4j:pass@localhost:7474/",
|
|
19
|
+
required: false
|
|
20
|
+
|
|
21
|
+
option :docker,
|
|
22
|
+
desc: "Generate a Docker Compose file for a Neo4j database",
|
|
23
|
+
type: :boolean,
|
|
24
|
+
default: true,
|
|
25
|
+
required: false
|
|
26
|
+
|
|
27
|
+
def call(project_name: , **options)
|
|
28
|
+
f = Dry::CLI::Utils::Files
|
|
29
|
+
root = Pathname.new(__FILE__).join("..")
|
|
30
|
+
|
|
31
|
+
if f.exist?("#{project_name}/.aspen")
|
|
32
|
+
raise RuntimeError, "There is already an Aspen project at #{project_name}, stopping."
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
URI.parse(options[:database_url])
|
|
36
|
+
|
|
37
|
+
puts "\nGenerated:"
|
|
38
|
+
puts "----------"
|
|
39
|
+
|
|
40
|
+
f.mkdir "#{project_name}/"
|
|
41
|
+
puts " #{project_name}/ -> Project folder"
|
|
42
|
+
|
|
43
|
+
f.touch "#{project_name}/.aspen"
|
|
44
|
+
puts " #{project_name}/.aspen -> File indicating Aspen project "
|
|
45
|
+
|
|
46
|
+
File.open("#{project_name}/manifest.yml", 'w') do |file|
|
|
47
|
+
template = get_template_file('manifest.yml.erb')
|
|
48
|
+
file << ERB.new(template).result_with_hash(project_name: project_name)
|
|
49
|
+
end
|
|
50
|
+
puts " #{project_name}/manifest.yml -> Metadata about included files"
|
|
51
|
+
|
|
52
|
+
f.touch "#{project_name}/config/db.yml"
|
|
53
|
+
File.open("#{project_name}/config/db.yml", 'w') do |file|
|
|
54
|
+
template = get_template_file('db.yml.erb')
|
|
55
|
+
file << ERB.new(template).result_with_hash(database_url: options[:database_url])
|
|
56
|
+
end
|
|
57
|
+
puts " #{project_name}/config/db.yml -> Database configuration"
|
|
58
|
+
|
|
59
|
+
if options[:docker]
|
|
60
|
+
f.cp get_template_path("docker-compose.yml"), "#{project_name}/docker-compose.yml"
|
|
61
|
+
puts " #{project_name}/docker-compose.yml -> Docker Compose file for Neo4j"
|
|
62
|
+
else
|
|
63
|
+
puts " Skipping Docker Compose file"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
f.mkdir "#{project_name}/src/"
|
|
67
|
+
puts " #{project_name}/src/ -> Source files"
|
|
68
|
+
|
|
69
|
+
f.mkdir "#{project_name}/bin/"
|
|
70
|
+
puts " #{project_name}/bin/ -> Binary files (scripts)"
|
|
71
|
+
|
|
72
|
+
f.cp get_template_path("convert"), "#{project_name}/bin/convert"
|
|
73
|
+
FileUtils.chmod("+x", "#{project_name}/bin/convert")
|
|
74
|
+
puts " #{project_name}/bin/convert -> Converts non-Aspen to Aspen"
|
|
75
|
+
|
|
76
|
+
f.mkdir "#{project_name}/src/discourses/"
|
|
77
|
+
f.touch "#{project_name}/src/discourses/.gitkeep"
|
|
78
|
+
puts " #{project_name}/src/discourses/ -> Collection of Discourses"
|
|
79
|
+
|
|
80
|
+
f.mkdir "#{project_name}/src/grammars/"
|
|
81
|
+
f.touch "#{project_name}/src/grammars/.gitkeep"
|
|
82
|
+
puts " #{project_name}/src/grammars/ -> Collection of Grammars"
|
|
83
|
+
|
|
84
|
+
f.mkdir "#{project_name}/build/"
|
|
85
|
+
f.touch "#{project_name}/build/.gitkeep"
|
|
86
|
+
puts " #{project_name}/build/ -> Compilation is output here"
|
|
87
|
+
|
|
88
|
+
f.cp get_template_path(".gitignore"), "#{project_name}/.gitignore"
|
|
89
|
+
f.touch "#{project_name}/.gitignore"
|
|
90
|
+
puts " #{project_name}/.gitignore -> Ignoring config files, build files"
|
|
91
|
+
|
|
92
|
+
if options[:docker]
|
|
93
|
+
puts "\nTo start the Neo4j database, run `docker-compose up` from the #{project_name} folder"
|
|
94
|
+
end
|
|
95
|
+
puts "\n✅ Generated new project '#{project_name}'\n\n"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get_template_file(name)
|
|
99
|
+
File.read(
|
|
100
|
+
get_template_path(name)
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def get_template_path(name)
|
|
105
|
+
File.expand_path(
|
|
106
|
+
File.join(
|
|
107
|
+
File.dirname(__FILE__), "..", "templates", name
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'aspen/actions/watch'
|
|
2
|
+
|
|
3
|
+
module Aspen
|
|
4
|
+
module CLI
|
|
5
|
+
module Commands
|
|
6
|
+
|
|
7
|
+
module Watch
|
|
8
|
+
class Run < Dry::CLI::Command
|
|
9
|
+
desc "Watch the project for changes; rebuild and push"
|
|
10
|
+
|
|
11
|
+
option :database,
|
|
12
|
+
type: :boolean,
|
|
13
|
+
default: true,
|
|
14
|
+
desc: "Push to sandbox database in config/db.yml",
|
|
15
|
+
aliases: ["d"]
|
|
16
|
+
|
|
17
|
+
example [
|
|
18
|
+
" # Recompiles and pushes to database",
|
|
19
|
+
"-D # Only recompiles files",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def call(**options)
|
|
23
|
+
Aspen::Actions::Watch.new(options: options).call
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require 'aspen/cli/commands/version'
|
|
2
|
+
require 'aspen/cli/commands/compile'
|
|
3
|
+
require 'aspen/cli/commands/new'
|
|
4
|
+
require 'aspen/cli/commands/push'
|
|
5
|
+
# require 'aspen/cli/commands/watch'
|
|
6
|
+
require 'aspen/cli/commands/build'
|
|
7
|
+
|
|
8
|
+
module Aspen
|
|
9
|
+
module CLI
|
|
10
|
+
module Commands
|
|
11
|
+
extend Dry::CLI::Registry
|
|
12
|
+
|
|
13
|
+
register "version", Version, aliases: ["v", "-v", "--version"]
|
|
14
|
+
register "compile", Compile, aliases: ["c"]
|
|
15
|
+
register "build", Build, aliases: ["b"]
|
|
16
|
+
register "push", Push, aliases: ["p"]
|
|
17
|
+
|
|
18
|
+
# register "watch", Watch::Run, aliases: ["w"]
|
|
19
|
+
|
|
20
|
+
register "new", New
|
|
21
|
+
|
|
22
|
+
# register "generate", aliases: ["g"] do |prefix|
|
|
23
|
+
# prefix.register "discourse", Generate::Discourse
|
|
24
|
+
# prefix.register "narrative", Generate::Narrative
|
|
25
|
+
# end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# api_key: your Airtable API key
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# This file is used to convert any non-Aspen resources,
|
|
4
|
+
# such as CSV files or other data, into Aspen before
|
|
5
|
+
# building the project.
|
|
6
|
+
|
|
7
|
+
require 'aspen'
|
|
8
|
+
require 'aspen/helpers'
|
|
9
|
+
require 'aspen/conversion'
|
|
10
|
+
|
|
11
|
+
include Aspen::Helpers
|
|
12
|
+
include Aspen::Conversion
|
|
13
|
+
|
|
14
|
+
# Aspen.convert.csv('src/a_spreadsheet.csv').to_aspen do |csv, aspen|
|
|
15
|
+
# TODO: Do conversion work here
|
|
16
|
+
# end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# WARNING!
|
|
2
|
+
# WARNING!
|
|
3
|
+
# WARNING!
|
|
4
|
+
|
|
5
|
+
# This should NEVER point to a production database or any
|
|
6
|
+
# database that you need the data to persist in.
|
|
7
|
+
|
|
8
|
+
# This should only ever point to a Sandbox database that
|
|
9
|
+
# you are comfortable completely deleting each time
|
|
10
|
+
# you rebuild these Aspen files.
|
|
11
|
+
|
|
12
|
+
<%
|
|
13
|
+
require 'uri'
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
uri = URI.parse(database_url)
|
|
17
|
+
rescue URI::InvalidURIError
|
|
18
|
+
raise "Invalid database URL"
|
|
19
|
+
end
|
|
20
|
+
%>
|
|
21
|
+
|
|
22
|
+
url: <%= database_url %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
services:
|
|
2
|
+
|
|
3
|
+
neo4j:
|
|
4
|
+
image: neo4j:3.5.11
|
|
5
|
+
volumes:
|
|
6
|
+
- ./db/data:/data
|
|
7
|
+
- ./db/plugins:/plugins
|
|
8
|
+
- ./db/logs:/logs
|
|
9
|
+
ports:
|
|
10
|
+
- 7474:7474
|
|
11
|
+
- 7687:7687
|
|
12
|
+
environment:
|
|
13
|
+
- NEO4J_AUTH=neo4j/pass
|
|
14
|
+
- NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
|
|
15
|
+
- NEO4JLABS_PLUGINS=["apoc"]
|
|
16
|
+
- NEO4J_apoc_export_file_enabled=true
|
|
17
|
+
- NEO4J_apoc_import_file_enabled=true
|
|
18
|
+
- NEO4J_apoc_import_file_use__neo4j__config=true
|
|
19
|
+
- NEO4J_dbms_security_procedures_whitelist=apoc.*
|
|
20
|
+
- NEO4J_dbms_security_procedures_unrestricted=apoc.*
|
|
21
|
+
|
|
22
|
+
volumes:
|
|
23
|
+
neo4j:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
project_name: <%= project_name %>
|
|
2
|
+
|
|
3
|
+
# These list the output formats that `aspen build` will produce.
|
|
4
|
+
# Remove lines to remove that file from the build.
|
|
5
|
+
output:
|
|
6
|
+
- Cypher
|
|
7
|
+
- JSON
|
|
8
|
+
- GEXF
|
|
9
|
+
|
|
10
|
+
# Uncomment to ignore files in src/ or src/grammars
|
|
11
|
+
# ignore:
|
|
12
|
+
# src:
|
|
13
|
+
# - specific_aspen_or_csv_file
|
|
14
|
+
# grammars:
|
|
15
|
+
# - default
|
|
16
|
+
|
|
17
|
+
# Attach a file from Airtable with the following format
|
|
18
|
+
# attached:
|
|
19
|
+
# -
|
|
20
|
+
# name: { Name of resource, displayed in prompt. Not connected to Airtable. }
|
|
21
|
+
# source: airtable
|
|
22
|
+
# app_id: { your app ID (API key) }
|
|
23
|
+
# table: { Name of table in Airtable }
|
|
24
|
+
# columns:
|
|
25
|
+
# - Columns
|
|
26
|
+
# - To
|
|
27
|
+
# - Download
|
|
28
|
+
|
|
29
|
+
# How long to wait before re-downloading files (in seconds)
|
|
30
|
+
# If no cache_seconds is specified, it will re-download every time.
|
|
31
|
+
# cache_seconds: 300
|
data/lib/aspen/cli.rb
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
require 'dry/monads'
|
|
2
|
+
include Dry::Monads[:maybe]
|
|
3
|
+
|
|
4
|
+
module Aspen
|
|
5
|
+
class Compiler
|
|
6
|
+
|
|
7
|
+
attr_reader :root, :environment
|
|
8
|
+
|
|
9
|
+
# @param environment [Hash]
|
|
10
|
+
# @todo Make {environment} an Aspen::Environment
|
|
11
|
+
def self.render(root, environment = {})
|
|
12
|
+
new(root, environment).render
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param environment [Hash]
|
|
16
|
+
# @todo Make {environment} an Aspen::Environment
|
|
17
|
+
def initialize(root, environment = {})
|
|
18
|
+
@root = root
|
|
19
|
+
@environment = environment
|
|
20
|
+
@adapter = environment.fetch(:adapter, :cypher).to_sym
|
|
21
|
+
# @todo FIXME: This is too much responsibility for the compiler.
|
|
22
|
+
# This should be delegated to an object and the later calls
|
|
23
|
+
# just messages to that object.
|
|
24
|
+
@slug_counters = Hash.new { 1 }
|
|
25
|
+
|
|
26
|
+
# @todo Move this into an Environment object—it should be set there.
|
|
27
|
+
# and here, just run environment.validate
|
|
28
|
+
unless Aspen.available_formats.include?(@adapter)
|
|
29
|
+
raise Aspen::ArgumentError, <<~MSG
|
|
30
|
+
The adapter, also known as the output format, must be one of:
|
|
31
|
+
#{Aspen.available_formats.join(', ')}.
|
|
32
|
+
|
|
33
|
+
What Aspen received was #{@adapter}.
|
|
34
|
+
MSG
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def render
|
|
39
|
+
visit(root)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def discourse
|
|
43
|
+
@discourse ||= Discourse.from_hash(environment)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def visit(node)
|
|
47
|
+
short_name = node.class.to_s.split('::').last.downcase
|
|
48
|
+
method_name = "visit_#{short_name}"
|
|
49
|
+
send(method_name, node)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def visit_narrative(node)
|
|
53
|
+
# Instead of letting comments be `nil` and using `#compact`
|
|
54
|
+
# to silently remove them, possibly hiding errors, we "compile"
|
|
55
|
+
# comments as `:comment` and filter them explicitly
|
|
56
|
+
statements = node.statements.map do |statement|
|
|
57
|
+
# This will visit both regular and custom statements.
|
|
58
|
+
visit(statement)
|
|
59
|
+
end.reject { |elem| elem == :comment }
|
|
60
|
+
|
|
61
|
+
renderer_klass = Kernel.const_get("Aspen::Renderers::#{@adapter.to_s.downcase.capitalize}Renderer")
|
|
62
|
+
renderer_klass.new(statements, environment).render
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_statement(node)
|
|
66
|
+
Statement.new(
|
|
67
|
+
origin: visit(node.origin),
|
|
68
|
+
edge: visit(node.edge),
|
|
69
|
+
target: visit(node.target)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @todo Get the labels back into here. Labelreg? typereg[:labels]?
|
|
74
|
+
# This is doing too much.
|
|
75
|
+
# Can't we have typed attributes come from the Grammar?
|
|
76
|
+
def visit_customstatement(node)
|
|
77
|
+
statement = visit(node.content)
|
|
78
|
+
matcher = discourse.grammar.matcher_for(statement)
|
|
79
|
+
results = matcher.captures(statement)
|
|
80
|
+
template = matcher.template
|
|
81
|
+
typereg = matcher.typereg
|
|
82
|
+
labelreg = matcher.labelreg
|
|
83
|
+
|
|
84
|
+
nodes = []
|
|
85
|
+
|
|
86
|
+
typed_results = results.inject({}) do |hash, elem|
|
|
87
|
+
key, value = elem
|
|
88
|
+
typed_value = case typereg[key]
|
|
89
|
+
when :integer then value.to_i
|
|
90
|
+
when :float then value.to_f
|
|
91
|
+
when :numeric then
|
|
92
|
+
value.match?(/\./) ? value.to_f : value.to_i
|
|
93
|
+
when :string then "\"#{value}\""
|
|
94
|
+
when :node then
|
|
95
|
+
# FIXME: This only handles short form.
|
|
96
|
+
# I think we were allowing grouped and Cypher form to fill
|
|
97
|
+
# in custom statement templates.
|
|
98
|
+
# TODO: Add some object to nodes array.
|
|
99
|
+
node = visit(
|
|
100
|
+
Aspen::AST::Nodes::Node.new(
|
|
101
|
+
attribute: value,
|
|
102
|
+
label: labelreg[key]
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
nodes << node
|
|
106
|
+
node
|
|
107
|
+
end
|
|
108
|
+
hash[key] = typed_value
|
|
109
|
+
hash
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
formatted_results = typed_results.inject({}) do |hash, elem|
|
|
113
|
+
key, value = elem
|
|
114
|
+
f_value = value.is_a?(Aspen::Node) ? value.nickname_node : value
|
|
115
|
+
hash[key] = f_value
|
|
116
|
+
|
|
117
|
+
# TODO: Trying to insert a p_id as well as p to be used in JSON identifiers.
|
|
118
|
+
# if value.is_a?(Aspen::Node)
|
|
119
|
+
# hash["#{key}_id"] = value.nickname
|
|
120
|
+
# end
|
|
121
|
+
# puts "TYPED VALS: #{hash.inspect}"
|
|
122
|
+
hash
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
slugs = template.scan(/{{{?(?<full>uniq_(?<name>\w+))}}}?/).uniq
|
|
126
|
+
usable_results = if slugs.any?
|
|
127
|
+
counts = slugs.map do |full, short|
|
|
128
|
+
[full, "#{short}_#{@slug_counters[full]}"]
|
|
129
|
+
end.to_h
|
|
130
|
+
|
|
131
|
+
context = results.merge(counts)
|
|
132
|
+
custom_statement = CustomStatement.new(
|
|
133
|
+
nodes: nodes,
|
|
134
|
+
cypher: Mustache.render(template.strip, formatted_results.merge(counts))
|
|
135
|
+
)
|
|
136
|
+
slugs.each do |full, _|
|
|
137
|
+
@slug_counters[full] = @slug_counters[full] + 1
|
|
138
|
+
end
|
|
139
|
+
custom_statement
|
|
140
|
+
else
|
|
141
|
+
CustomStatement.new(
|
|
142
|
+
nodes: nodes,
|
|
143
|
+
cypher: Mustache.render(template.strip, formatted_results)
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def visit_node(node)
|
|
149
|
+
# Get the label, falling back to the default label.
|
|
150
|
+
label = visit(node.label)
|
|
151
|
+
|
|
152
|
+
# Get the attribute name, falling back to the default attribute name.
|
|
153
|
+
attribute_name = Maybe(nil).value_or(discourse.default_attr_name(label))
|
|
154
|
+
typed_attribute_value = visit(node.attribute)
|
|
155
|
+
nickname = typed_attribute_value.to_s.downcase
|
|
156
|
+
|
|
157
|
+
Aspen::Node.new(
|
|
158
|
+
label: label,
|
|
159
|
+
attributes: { attribute_name => typed_attribute_value }
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def visit_edge(node)
|
|
164
|
+
content = visit(node.content)
|
|
165
|
+
unless discourse.allows_edge?(content)
|
|
166
|
+
raise Aspen::Error, """
|
|
167
|
+
Your narrative includes an edge called '#{content}',
|
|
168
|
+
but only #{discourse.allowed_edges} are allowed.
|
|
169
|
+
"""
|
|
170
|
+
end
|
|
171
|
+
Aspen::Edge.new(
|
|
172
|
+
content,
|
|
173
|
+
mutual: discourse.mutual?(visit(node.content))
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def visit_label(node)
|
|
178
|
+
content = visit(node.content)
|
|
179
|
+
label = Maybe(content).value_or(discourse.default_label)
|
|
180
|
+
unless discourse.allows_label?(label)
|
|
181
|
+
raise Aspen::CompileError, """
|
|
182
|
+
Your narrative includes a node with label '#{label}',
|
|
183
|
+
but only #{discourse.allowed_labels} are allowed.
|
|
184
|
+
"""
|
|
185
|
+
end
|
|
186
|
+
label
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def visit_attribute(node)
|
|
190
|
+
content = visit(node.content)
|
|
191
|
+
type = visit(node.type)
|
|
192
|
+
content.send(type.converter)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def visit_type(node)
|
|
196
|
+
node
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def visit_content(node)
|
|
200
|
+
node.content
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @returns [Symbol] :comment
|
|
204
|
+
# This acts as a signal so other methods know to reject comments.
|
|
205
|
+
def visit_comment(node)
|
|
206
|
+
:comment
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'dry/validation'
|
|
2
|
+
|
|
3
|
+
module Aspen
|
|
4
|
+
module Contracts
|
|
5
|
+
class DefaultAttributeContract < Dry::Validation::Contract
|
|
6
|
+
|
|
7
|
+
LABEL = /^[A-Z]{1}\w+$/
|
|
8
|
+
ATTR_NAME = /^[a-z]{1}[a-z_]*[a-z]{1}$/
|
|
9
|
+
|
|
10
|
+
schema do
|
|
11
|
+
required(:label).value(:string)
|
|
12
|
+
required(:attr_name).value(:string)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rule(:label) do
|
|
16
|
+
unless LABEL.match?(value)
|
|
17
|
+
key.failure("must be a valid Neo4j label (one TitleCase word), was #{value}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
rule(:attr_name) do
|
|
22
|
+
unless ATTR_NAME.match?(value)
|
|
23
|
+
key.failure("must be a single token in snake_case (lowercase and underscores only), was: #{value}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'aspen/contracts/default_attribute_contract'
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'csv'
|
|
2
|
+
|
|
3
|
+
module Aspen
|
|
4
|
+
# Helps convert non-Aspen into Aspen
|
|
5
|
+
# before compiling Aspen into other code.
|
|
6
|
+
module Conversion
|
|
7
|
+
class Builder
|
|
8
|
+
def initialize(args = {})
|
|
9
|
+
@from_format = args[:format]
|
|
10
|
+
@from_file = args[:file]
|
|
11
|
+
@csv_options = { headers: true }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def csv(path)
|
|
15
|
+
@from_format = :csv
|
|
16
|
+
@from_path = path
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def tsv(path)
|
|
21
|
+
@from_format = :csv
|
|
22
|
+
@from_path = path
|
|
23
|
+
@csv_options[:col_sep] = "\t"
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_aspen(&block)
|
|
28
|
+
file = CSV.open(@from_path, @csv_options)
|
|
29
|
+
aspen = File.open(@from_path.rpartition(".").first + ".aspen", 'w')
|
|
30
|
+
yield file, aspen
|
|
31
|
+
ensure
|
|
32
|
+
aspen.close
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @example
|
|
38
|
+
# Aspen.convert.csv('path/to/csv').to_aspen
|
|
39
|
+
def self.convert
|
|
40
|
+
Aspen::Conversion::Builder.new({})
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Aspen
|
|
2
|
+
module CustomGrammar
|
|
3
|
+
module AST
|
|
4
|
+
module Nodes
|
|
5
|
+
class CaptureSegment
|
|
6
|
+
|
|
7
|
+
attr_reader :type, :var_name, :label
|
|
8
|
+
|
|
9
|
+
def initialize(type: , var_name: , label: )
|
|
10
|
+
@type = type
|
|
11
|
+
@var_name = var_name
|
|
12
|
+
@label = label
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|