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,55 @@
1
+ require 'listen'
2
+
3
+ module Aspen
4
+ module Actions
5
+ class Watch
6
+
7
+ def initialize(options: {})
8
+ @options = options
9
+ @logger = options.fetch(:logger) { Logger.new(STDOUT, level: :warn) }
10
+ Listen.logger = @logger
11
+ end
12
+
13
+ def call
14
+ puts warning_message if using_database?
15
+
16
+ listener = Listen.to('src/', only: /\.aspen$/) do |mod, add, _rem|
17
+ Aspen::CLI::Commands::Build.new.call
18
+ Aspen::Actions::Push.new.call
19
+ rescue Aspen::Error => e
20
+ puts e.message
21
+ end
22
+
23
+ listener.start
24
+ sleep
25
+ rescue Interrupt => e
26
+ # FIXME: The logger calls don't ever seem to work.
27
+ @logger.info "Exiting..."
28
+ puts "\nExiting..."
29
+ listener.stop
30
+ raise SystemExit
31
+ end
32
+
33
+ private
34
+
35
+ def using_database?
36
+ @options[:database]
37
+ end
38
+
39
+ def warning_message
40
+ <<~MSG
41
+ ⚠️ WARNING: `aspen watch` is experimental, and saving a file/rebuilding the data
42
+ will cause ALL OF THE DATA in the database to be DELETED and rewritten.
43
+
44
+ By proceeding, you acknowledge that the Neo4j database at TODO: host:port will
45
+ be dropped the next time a file in this project is saved.
46
+
47
+ Use Ctrl+C to quit.
48
+
49
+ Watching....
50
+ MSG
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ module Aspen
2
+ module Actions
3
+ end
4
+ end
5
+
6
+ require 'aspen/actions/compile'
7
+ require 'aspen/actions/push'
8
+ require 'aspen/actions/watch'
@@ -0,0 +1,38 @@
1
+ module Aspen
2
+ module Adapters
3
+
4
+ class Adapter
5
+ attr_reader :id, :name, :ext, :renderer
6
+
7
+ def initialize(id: , name: , ext: )
8
+ @id = id
9
+ @name = name
10
+ @ext = ext
11
+ # @todo This will be buggy if we have a two-word class
12
+ @renderer = Kernel.const_get("Aspen::Renderers::#{@name.downcase.capitalize}Renderer")
13
+ end
14
+ end
15
+
16
+ class Registry
17
+
18
+ attr_reader :data
19
+
20
+ def initialize
21
+ @data ||= {
22
+ cypher: Adapter.new(id: :cypher, name: "Cypher", ext: '.cql' ),
23
+ json: Adapter.new(id: :json, name: "JSON", ext: '.json'),
24
+ gexf: Adapter.new(id: :gexf, name: "GEXF", ext: '.gexf'),
25
+ }
26
+ Aspen.available_formats = @data.keys
27
+ end
28
+
29
+ def self.get(key)
30
+ # @todo There's a better design for this.
31
+ @@store ||= new.data
32
+ @@store.fetch(key)
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Attribute
5
+
6
+ attr_reader :content, :type
7
+
8
+ def initialize(content)
9
+ @content = Aspen::AST::Nodes::Content.new(content)
10
+ @type = Aspen::AST::Nodes::Type.determine(content)
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Comment
5
+
6
+ attr_reader :content
7
+
8
+ def initialize(content)
9
+ @content = Aspen::AST::Nodes::Content.new(content)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Content
5
+
6
+ attr_reader :content
7
+
8
+ def initialize(content)
9
+ @content = content
10
+ end
11
+
12
+ alias_method :inner_content, :content
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class CustomStatement
5
+
6
+ attr_reader :content
7
+
8
+ # I wonder: Should the AST for CustomGrammar be grouped
9
+ # into lib/ast? Should the CustomStatement AST node contain
10
+ # the Bare Segments and Capture Segments, along with
11
+ # variables with types and labels?
12
+ def initialize(content)
13
+ @content = Aspen::AST::Nodes::Content.new(content)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Edge
5
+
6
+ attr_reader :content
7
+
8
+ def initialize(content)
9
+ @content = Aspen::AST::Nodes::Content.new(content)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Label
5
+
6
+ attr_reader :content
7
+
8
+ def initialize(content)
9
+ @content = Aspen::AST::Nodes::Content.new(content)
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Narrative
5
+
6
+ attr_reader :statements
7
+
8
+ def initialize(statements)
9
+ @statements = statements
10
+ end
11
+
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,20 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Node
5
+
6
+ attr_reader :attribute, :label
7
+
8
+ def initialize(attribute: , label: nil)
9
+ @attribute = Aspen::AST::Nodes::Attribute.new(attribute)
10
+ @label = Aspen::AST::Nodes::Label.new(label)
11
+ end
12
+
13
+ def label=(content)
14
+ @label = Aspen::AST::Nodes::Label.new(content)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Statement
5
+
6
+ attr_reader :origin, :edge, :target
7
+
8
+ def initialize(origin: nil, edge: nil, target: nil)
9
+ @origin = origin
10
+ @edge = edge
11
+ @target = target
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ class Type
5
+
6
+ STRING = "STRING"
7
+ INTEGER = "INTEGER"
8
+ FLOAT = "FLOAT"
9
+
10
+ MATCH_FLOAT = /^([\d,]+\.\d+)$/
11
+ MATCH_INTEGER = /^([\d,]+)$/
12
+ MATCH_STRING = /^(.+)$/
13
+
14
+ def self.determine(value)
15
+ new(
16
+ case value
17
+ when MATCH_FLOAT then FLOAT
18
+ when MATCH_INTEGER then INTEGER
19
+ when MATCH_STRING then STRING
20
+ else
21
+ raise ArgumentError, "Could not determine a type for value:\n\t#{value.inspect}"
22
+ end
23
+ )
24
+ end
25
+
26
+ attr_reader :content, :converter
27
+
28
+ def initialize(type_const)
29
+ @content = Aspen::AST::Nodes::Content.new(type_const)
30
+ @converter = get_converter(type_const)
31
+ end
32
+
33
+ def get_converter(type_const)
34
+ case type_const
35
+ when FLOAT then :to_f
36
+ when INTEGER then :to_i
37
+ when STRING then :to_s
38
+ else
39
+ raise ArgumentError, "Could not determine a converter method for type:\n\t#{value.inspect}"
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/aspen/ast.rb ADDED
@@ -0,0 +1,18 @@
1
+ module Aspen
2
+ module AST
3
+ module Nodes
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'aspen/ast/nodes/attribute'
9
+ require 'aspen/ast/nodes/type'
10
+
11
+ require 'aspen/ast/nodes/comment'
12
+ require 'aspen/ast/nodes/content'
13
+ require 'aspen/ast/nodes/edge'
14
+ require 'aspen/ast/nodes/label'
15
+ require 'aspen/ast/nodes/narrative'
16
+ require 'aspen/ast/nodes/node'
17
+ require 'aspen/ast/nodes/statement'
18
+ require 'aspen/ast/nodes/custom_statement'
@@ -0,0 +1,26 @@
1
+ require 'csv'
2
+ require 'aspen/cli/commands/build_steps'
3
+
4
+ module Aspen
5
+ module CLI
6
+ module Commands
7
+
8
+ class Build < Dry::CLI::Command
9
+ desc "Build Aspen project"
10
+
11
+ option :batch, type: :boolean, desc: "Batching", default: true
12
+
13
+ include BuildSteps
14
+
15
+ def call(**options)
16
+ CheckAspenProject.new.call
17
+ DownloadAttachedResources.new.call
18
+ ConvertIntoAspen.new.call
19
+ main_aspen_file = CollectMainAspen.new.call
20
+ CompileMainAspen.new.call(main_aspen_file, options)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,204 @@
1
+ require 'airtable'
2
+
3
+ module Aspen
4
+ module CLI
5
+ module Commands
6
+ module BuildSteps
7
+
8
+ class BuildStep
9
+ def manifest
10
+ return @manifest if @manifest
11
+ @manifest = @files.exist?('manifest.yml') ? YAML.load_file('manifest.yml') : {}
12
+ end
13
+
14
+ def config
15
+ return @config if @config
16
+ @config = {}
17
+ Dir['config/*.yml'].each do |path|
18
+ key = File.basename(path, File.extname(path))
19
+ value = YAML.load_file(path)
20
+ @config[key.to_s] = value
21
+ end
22
+ @config
23
+ end
24
+
25
+ def call(*)
26
+ @files = Dry::CLI::Utils::Files
27
+ end
28
+ end
29
+
30
+
31
+ class CheckAspenProject < BuildStep
32
+ # Check if we're in an Aspen project, and stop if not.
33
+ def call(*)
34
+ super
35
+ unless @files.exist?('.aspen')
36
+ raise ArgumentError, "Must be within an Aspen project to run `build`"
37
+ end
38
+ end
39
+ end # / CheckAspenProject
40
+
41
+
42
+ class DownloadAttachedResources < BuildStep
43
+ def call(*)
44
+ super
45
+ return false unless manifest['attached']
46
+ check_for_save_file
47
+ if cache_expired?
48
+ download
49
+ update_save_file!
50
+ else
51
+ puts "----> Skipping download, cache is still good."
52
+ end
53
+ end
54
+
55
+ def check_for_save_file
56
+ unless @files.exist?('build/last-download')
57
+ File.open('build/last-download', 'w') { |f| f << 0 }
58
+ end
59
+ end
60
+
61
+ def cache_expired?
62
+ if manifest["cache_seconds"]
63
+ last = File.read('build/last-download').to_i # Last download time
64
+ time_since_last_download = (Time.now - last).to_i # Time since last download
65
+ threshhold = manifest['cache_seconds'].to_i # Cache threshhold
66
+ time_since_last_download > threshhold
67
+ end
68
+ end
69
+
70
+ def update_save_file!
71
+ File.open('build/last-download', 'w') { |f| f << Time.now.to_i }
72
+ end
73
+
74
+ def download
75
+ puts '----> Downloading attached resources'
76
+ manifest['attached'].each do |resource|
77
+ puts " > Downloading #{resource["name"]} (#{resource["source"].capitalize})"
78
+ case resource["source"]
79
+ when "airtable" then download_airtable_resource(resource)
80
+ end
81
+ end
82
+ end
83
+
84
+ def download_airtable_resource(resource)
85
+ client = Airtable::Client.new(config.dig('airtable', 'api_key'))
86
+ table = client.table(resource['app_id'], resource['table'])
87
+ out_path = "src/#{resource["name"].downcase.gsub(" ", "_")}.csv"
88
+ CSV.open(out_path, 'w') do |file|
89
+ columns = Array(resource['columns'])
90
+ file << columns
91
+ table.records.each do |record|
92
+ file << columns.map { |col| record[col] }
93
+ end
94
+ end
95
+ end
96
+
97
+ end # / DownloadAttachedResources
98
+
99
+ class ConvertIntoAspen < BuildStep
100
+ def call(*)
101
+ super
102
+ puts "----> Converting non-Aspen to Aspen (bin/convert)"
103
+ if @files.exist?('bin/convert')
104
+ unless system('bin/convert')
105
+ raise RuntimeError, "`bin/convert` didn't work, stopping build. See above Traceback."
106
+ end
107
+ end
108
+ end
109
+ end # / ConvertIntoAspen
110
+
111
+
112
+ class CollectMainAspen < BuildStep
113
+ def call(*)
114
+ super
115
+ # Grab all the grammars and Aspen source files, make into one main Aspen file.
116
+ puts "----> Collecting main.aspen from src/ and src/grammars"
117
+
118
+ # Clear the build/ folder
119
+ Dir["build/main-*"].each { |path| @files.delete(path)}
120
+
121
+ @grammars = "" # Main grammars IO
122
+ @discourses = "" # Main discourses IO
123
+ @aspens = "" # Main Aspen IO
124
+
125
+ collect_discourses
126
+ collect_grammars
127
+ collect_aspen
128
+
129
+ main_aspen = File.open("build/main-#{Time.now.to_i}.aspen", 'w') do |file|
130
+ file << @discourses
131
+ file << @grammars
132
+ file << Aspen::SEPARATOR + "\n"
133
+ file << @aspens
134
+ end
135
+ return main_aspen
136
+ end
137
+
138
+ def collect_discourses
139
+ Dir['src/discourses/*.aspen'].map do |path|
140
+ # Skip if there's an ignore: src: in the manifest, and if it matches the file path
141
+ next if manifest.dig("ignore", "discourses") && manifest.dig("ignore", "discourses").any? { |ignore_path| Regexp.new(ignore_path) =~ path }
142
+ @discourses << File.read(path)
143
+ end
144
+ end
145
+
146
+ def collect_grammars
147
+ Dir['src/grammars/*.aspen'].map do |path|
148
+ # Skip if there's an ignore: src: in the manifest, and if it matches the file path
149
+ next if manifest.dig("ignore", "grammars") && manifest.dig("ignore", "grammars").any? { |ignore_path| Regexp.new(ignore_path) =~ path }
150
+ @grammars << File.read(path)
151
+ end
152
+ end
153
+
154
+ def collect_aspen
155
+ Dir['src/*.aspen'].map do |path|
156
+ next if manifest.dig("ignore", "src") && manifest.dig("ignore", "src").any? do |ignore_path|
157
+ Regexp.new(ignore_path) =~ path
158
+ end
159
+ File.read(path)
160
+ end.compact.each do |file|
161
+ if file.include?(SEPARATOR)
162
+ env, _sep, code = file.partition(SEPARATOR)
163
+ @discourses << env
164
+ @aspens << code
165
+ else
166
+ @aspens << file
167
+ end
168
+ end
169
+ end
170
+ end # / CollectMainAspen
171
+
172
+
173
+ class CompileMainAspen < BuildStep
174
+
175
+ def call(main_aspen_file, options)
176
+ super
177
+ puts "----> Compiling main Aspen file (#{main_aspen_file.path})"
178
+ # Compile the main Aspen file, according to manifest.yml
179
+ unless manifest["output"]
180
+ puts " Defaulting to Cypher output. To customize, set up a manifest.yml."
181
+ end
182
+
183
+ outputs = manifest["output"] ? Array(manifest["output"]) : ["Cypher"]
184
+
185
+ outputs.each do |format|
186
+ adapter = Aspen::Adapters::Registry.get(format.downcase.to_sym)
187
+ out_path = main_aspen_file.path.gsub(/\.aspen$/, '') + adapter.ext
188
+
189
+ File.open(out_path, 'w') do |file|
190
+ file << Aspen.compile_text(
191
+ File.read(main_aspen_file),
192
+ adapter: adapter.id,
193
+ batch: options[:batch]
194
+ )
195
+ end
196
+ puts "----> Compiled main #{adapter.name} file"
197
+ end
198
+ end
199
+ end # / CompileMainAspen
200
+
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,27 @@
1
+ require 'aspen/actions/compile'
2
+
3
+ module Aspen
4
+ module CLI
5
+ module Commands
6
+
7
+ class Compile < Dry::CLI::Command
8
+ desc "Compile an Aspen file to Cypher (in the same directory)"
9
+
10
+ argument :path, required: true, desc: "Aspen file to compile to Cypher"
11
+
12
+ option :database, type: :string, desc: "Database URL", aliases: ["d"]
13
+ # option :mode, default: "http", values: %w[http https bolt], desc: "The connection protocol"
14
+
15
+ example [
16
+ "path/to/file.aspen # Compiles to path/to/file.cypher",
17
+ "path/to/file.aspen -d http://neo4j:pass@localhost:11002 # Compiles and runs in database"
18
+ ]
19
+
20
+ def call(path: , **options)
21
+ Aspen::Actions::Compile.new(path, options).call
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Aspen
2
+ module CLI
3
+ module Commands
4
+
5
+ module Generate
6
+ class Discourse < Dry::CLI::Command
7
+ desc "Generate a new discourse within a project"
8
+ def call()
9
+ # NO OP
10
+ end
11
+ end
12
+
13
+ class Narrative < Dry::CLI::Command
14
+ desc "Generate a new narrative within a project"
15
+ def call()
16
+ # NO OP
17
+ end
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ end