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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +12 -0
  3. data/.gitignore +16 -0
  4. data/.travis.yml +6 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +175 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +84 -0
  10. data/Rakefile +10 -0
  11. data/aspen.gemspec +48 -0
  12. data/bin/aspen +3 -0
  13. data/bin/console +22 -0
  14. data/bin/setup +8 -0
  15. data/lib/aspen/abstract_parser.rb +72 -0
  16. data/lib/aspen/abstract_statement.rb +34 -0
  17. data/lib/aspen/actions/compile.rb +31 -0
  18. data/lib/aspen/actions/push.rb +39 -0
  19. data/lib/aspen/actions/watch.rb +55 -0
  20. data/lib/aspen/actions.rb +8 -0
  21. data/lib/aspen/adapters.rb +38 -0
  22. data/lib/aspen/ast/nodes/attribute.rb +16 -0
  23. data/lib/aspen/ast/nodes/comment.rb +15 -0
  24. data/lib/aspen/ast/nodes/content.rb +17 -0
  25. data/lib/aspen/ast/nodes/custom_statement.rb +19 -0
  26. data/lib/aspen/ast/nodes/edge.rb +15 -0
  27. data/lib/aspen/ast/nodes/label.rb +15 -0
  28. data/lib/aspen/ast/nodes/narrative.rb +15 -0
  29. data/lib/aspen/ast/nodes/node.rb +20 -0
  30. data/lib/aspen/ast/nodes/statement.rb +17 -0
  31. data/lib/aspen/ast/nodes/type.rb +46 -0
  32. data/lib/aspen/ast.rb +18 -0
  33. data/lib/aspen/cli/commands/build.rb +26 -0
  34. data/lib/aspen/cli/commands/build_steps.rb +204 -0
  35. data/lib/aspen/cli/commands/compile.rb +27 -0
  36. data/lib/aspen/cli/commands/generate.rb +23 -0
  37. data/lib/aspen/cli/commands/new.rb +115 -0
  38. data/lib/aspen/cli/commands/push.rb +15 -0
  39. data/lib/aspen/cli/commands/version.rb +15 -0
  40. data/lib/aspen/cli/commands/watch.rb +30 -0
  41. data/lib/aspen/cli/commands.rb +28 -0
  42. data/lib/aspen/cli/templates/.gitignore +6 -0
  43. data/lib/aspen/cli/templates/airtable.yml +1 -0
  44. data/lib/aspen/cli/templates/convert +16 -0
  45. data/lib/aspen/cli/templates/db.yml.erb +22 -0
  46. data/lib/aspen/cli/templates/docker-compose.yml +23 -0
  47. data/lib/aspen/cli/templates/manifest.yml.erb +31 -0
  48. data/lib/aspen/cli.rb +9 -0
  49. data/lib/aspen/compiler.rb +209 -0
  50. data/lib/aspen/contracts/default_attribute_contract.rb +29 -0
  51. data/lib/aspen/contracts.rb +1 -0
  52. data/lib/aspen/conversion.rb +43 -0
  53. data/lib/aspen/custom_grammar/ast/nodes/bare.rb +17 -0
  54. data/lib/aspen/custom_grammar/ast/nodes/capture_segment.rb +19 -0
  55. data/lib/aspen/custom_grammar/ast/nodes/content.rb +17 -0
  56. data/lib/aspen/custom_grammar/ast/nodes/expression.rb +17 -0
  57. data/lib/aspen/custom_grammar/ast.rb +13 -0
  58. data/lib/aspen/custom_grammar/compiler.rb +80 -0
  59. data/lib/aspen/custom_grammar/grammar.rb +78 -0
  60. data/lib/aspen/custom_grammar/lexer.rb +76 -0
  61. data/lib/aspen/custom_grammar/matcher.rb +43 -0
  62. data/lib/aspen/custom_grammar/parser.rb +51 -0
  63. data/lib/aspen/custom_grammar.rb +23 -0
  64. data/lib/aspen/custom_statement.rb +35 -0
  65. data/lib/aspen/discourse.rb +122 -0
  66. data/lib/aspen/edge.rb +35 -0
  67. data/lib/aspen/errors.rb +158 -0
  68. data/lib/aspen/helpers.rb +17 -0
  69. data/lib/aspen/lexer.rb +195 -0
  70. data/lib/aspen/list.rb +19 -0
  71. data/lib/aspen/node.rb +53 -0
  72. data/lib/aspen/parser.rb +183 -0
  73. data/lib/aspen/renderers/abstract_renderer.rb +22 -0
  74. data/lib/aspen/renderers/cypher_base_renderer.rb +36 -0
  75. data/lib/aspen/renderers/cypher_batch_renderer.rb +55 -0
  76. data/lib/aspen/renderers/cypher_renderer.rb +18 -0
  77. data/lib/aspen/renderers/gexf_renderer.rb +47 -0
  78. data/lib/aspen/renderers/json_renderer.rb +40 -0
  79. data/lib/aspen/renderers.rb +9 -0
  80. data/lib/aspen/schemas/discourse_schema.rb +64 -0
  81. data/lib/aspen/schemas/grammar_schema.rb +24 -0
  82. data/lib/aspen/statement.rb +42 -0
  83. data/lib/aspen/system_default.rb +12 -0
  84. data/lib/aspen/version.rb +3 -0
  85. data/lib/aspen.rb +65 -0
  86. 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,15 @@
1
+ module Aspen
2
+ module CLI
3
+ module Commands
4
+
5
+ class Push < Dry::CLI::Command
6
+ desc "Push latest Cypher"
7
+
8
+ def call(*)
9
+ Aspen::Actions::Push.new.call
10
+ end
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Aspen
2
+ module CLI
3
+ module Commands
4
+
5
+ class Version < Dry::CLI::Command
6
+ desc "Print version"
7
+
8
+ def call(*)
9
+ puts Aspen::VERSION
10
+ end
11
+ end
12
+
13
+ end
14
+ end
15
+ 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,6 @@
1
+ # Build folder
2
+ build/
3
+
4
+ # Authorization secrets
5
+ .env
6
+ config/*.yml
@@ -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,9 @@
1
+ require 'bundler/setup'
2
+ require 'dry/cli'
3
+ require 'dry/cli/utils/files'
4
+
5
+ require 'listen'
6
+ require 'aspen'
7
+
8
+ require 'aspen/cli/commands'
9
+ require 'aspen/actions'
@@ -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,17 @@
1
+ module Aspen
2
+ module CustomGrammar
3
+ module AST
4
+ module Nodes
5
+ class Bare
6
+
7
+ attr_reader :content
8
+
9
+ def initialize(content)
10
+ @content = Content.new(content)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
17
+ 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
@@ -0,0 +1,17 @@
1
+ module Aspen
2
+ module CustomGrammar
3
+ module AST
4
+ module Nodes
5
+ class Content
6
+
7
+ attr_reader :content
8
+
9
+ def initialize(content)
10
+ @content = content
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Aspen
2
+ module CustomGrammar
3
+ module AST
4
+ module Nodes
5
+ class Expression
6
+
7
+ attr_reader :segments
8
+
9
+ def initialize(segments=[])
10
+ @segments = segments
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end