lkml 0.1.0 → 0.2.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.
data/lib/lkml/utils.rb ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lkml
4
+ module Utils
5
+ module_function
6
+
7
+ def strip(str)
8
+ start = 0
9
+ end_idx = -1
10
+ nchars = str.length
11
+
12
+ start += 1 while start < nchars && str[start].match?(/\s/)
13
+
14
+ end_idx -= 1 while start != nchars && str[end_idx].match?(/\s/)
15
+
16
+ lspace = start.zero? ? "" : str[0, start]
17
+ rspace = if end_idx == -1
18
+ ""
19
+ else
20
+ str[(end_idx + 1)..] || ""
21
+ end
22
+
23
+ core = if end_idx == -1
24
+ str[start..] || ""
25
+ else
26
+ str[start..end_idx] || ""
27
+ end
28
+
29
+ [lspace, core, rspace]
30
+ end
31
+ end
32
+ end
data/lib/lkml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lkml
4
- VERSION = '0.1.0'
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/lkml/visitors.rb CHANGED
@@ -1,91 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
4
-
5
- require_relative 'tree'
3
+ require_relative "tree"
6
4
 
7
5
  module Lkml
8
- class BasicVisitor < Visitor
9
- def _visit(node)
10
- if node.is_a?(SyntaxToken)
11
- nil
12
- elsif node.respond_to?(:children) && node.children
13
- node.children.each { |child| child.accept(self) }
6
+ module Visitors
7
+ class BasicVisitor
8
+ include Tree::Visitor
9
+
10
+ def _visit(node)
11
+ case node
12
+ when Tree::SyntaxToken
13
+ nil
14
+ else
15
+ ch = node.respond_to?(:children) ? node.children : nil
16
+ ch&.each { |child| child.accept(self) }
17
+ end
14
18
  end
15
- end
16
19
 
17
- def visit(document)
18
- _visit(document)
19
- end
20
+ def visit(document)
21
+ _visit(document)
22
+ end
20
23
 
21
- def visit_container(node)
22
- _visit(node)
23
- end
24
+ def visit_container(node)
25
+ _visit(node)
26
+ end
24
27
 
25
- def visit_block(node)
26
- _visit(node)
27
- end
28
+ def visit_block(node)
29
+ _visit(node)
30
+ end
28
31
 
29
- def visit_list(node)
30
- _visit(node)
31
- end
32
+ def visit_list(node)
33
+ _visit(node)
34
+ end
32
35
 
33
- def visit_pair(node)
34
- _visit(node)
35
- end
36
+ def visit_pair(node)
37
+ _visit(node)
38
+ end
36
39
 
37
- def visit_token(token)
38
- _visit(token)
40
+ def visit_token(token)
41
+ _visit(token)
42
+ end
39
43
  end
40
- end
41
44
 
42
- class LookMlVisitor < BasicVisitor
43
- def _visit(node)
44
- node.to_s
45
+ class LookMlVisitor < BasicVisitor
46
+ def _visit(node)
47
+ node.to_s
48
+ end
45
49
  end
46
- end
47
50
 
48
- class BasicTransformer < Visitor
49
- def _visit_items(node)
50
- if node.respond_to?(:children) && node.children
51
- new_children = node.children.map { |child| child.accept(self) }
52
- node.class.new(items: new_children)
53
- else
54
- node
51
+ class BasicTransformer
52
+ include Tree::Visitor
53
+
54
+ def _visit_items(node)
55
+ ch = node.children
56
+ return node unless ch && !ch.empty?
57
+
58
+ new_children = ch.map { |child| child.accept(self) }
59
+ case node
60
+ when Tree::ContainerNode, Tree::ListNode
61
+ node.with(items: new_children.freeze)
62
+ else
63
+ node
64
+ end
55
65
  end
56
- end
57
66
 
58
- def _visit_container(node)
59
- if node.respond_to?(:container) && node.container
60
- new_child = node.container.accept(self)
61
- node.class.new(container: new_child)
62
- else
63
- node
67
+ def visit(document)
68
+ return document unless document.container
69
+
70
+ document.with(container: document.container.accept(self))
64
71
  end
65
- end
66
72
 
67
- def visit(node)
68
- _visit_container(node)
69
- end
73
+ def visit_container(node)
74
+ _visit_items(node)
75
+ end
70
76
 
71
- def visit_container(node)
72
- _visit_items(node)
73
- end
77
+ def visit_list(node)
78
+ _visit_items(node)
79
+ end
74
80
 
75
- def visit_list(node)
76
- _visit_items(node)
77
- end
81
+ def visit_block(node)
82
+ return node unless node.container
78
83
 
79
- def visit_block(node)
80
- _visit_container(node)
81
- end
84
+ node.with(container: node.container.accept(self))
85
+ end
82
86
 
83
- def visit_pair(node)
84
- node
85
- end
87
+ def visit_pair(node)
88
+ node
89
+ end
86
90
 
87
- def visit_token(token)
88
- token
91
+ def visit_token(token)
92
+ token
93
+ end
89
94
  end
90
95
  end
91
96
  end
data/lib/lkml.rb CHANGED
@@ -1,26 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lkml/keys'
4
- require_relative 'lkml/lexer'
5
- require_relative 'lkml/parser'
6
- require_relative 'lkml/simple'
7
- require_relative 'lkml/tokens'
8
- require_relative 'lkml/tree'
9
- require_relative 'lkml/visitors'
3
+ require_relative "lkml/version"
4
+ require_relative "lkml/tokens"
5
+ require_relative "lkml/keys"
6
+ require_relative "lkml/utils"
7
+ require_relative "lkml/tree"
8
+ require_relative "lkml/visitors"
9
+ require_relative "lkml/lexer"
10
+ require_relative "lkml/parser"
11
+ require "stringio"
12
+
13
+ require_relative "lkml/simple"
10
14
 
11
15
  module Lkml
12
- def self.parse(text)
16
+ module_function
17
+
18
+ def parse(text)
13
19
  lexer = Lexer.new(text)
14
20
  tokens = lexer.scan
15
21
  Parser.new(tokens).parse
16
22
  end
17
23
 
18
- def self.dump(obj, io = nil)
19
- parser = DictParser.new
20
- result = parser.parse(obj).to_s
21
- return result unless io
24
+ def load(stream)
25
+ text =
26
+ case stream
27
+ when String then stream
28
+ else
29
+ raise TypeError, "Input stream must be a string or file object." unless stream.respond_to?(:read)
30
+
31
+ stream.read
22
32
 
23
- io.write(result)
24
- nil
33
+ end
34
+ tree = parse(text)
35
+ Simple::DictVisitor.new.visit(tree)
36
+ end
37
+
38
+ def dump(obj, io: nil)
39
+ tree = Simple::DictParser.new.parse(obj)
40
+ result = tree.to_s
41
+ if io
42
+ io.write(result)
43
+ nil
44
+ else
45
+ result
46
+ end
47
+ end
48
+
49
+ alias generate dump
50
+
51
+ def dict_parser
52
+ Simple::DictParser
53
+ end
54
+
55
+ def dict_visitor
56
+ Simple::DictVisitor
25
57
  end
26
58
  end
59
+
60
+ Lkml.const_set(:DictParser, Lkml::Simple::DictParser)
61
+ Lkml.const_set(:DictVisitor, Lkml::Simple::DictVisitor)
data/lkml.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/lkml/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "lkml"
7
+ spec.version = Lkml::VERSION
8
+ spec.authors = ["Josh Temple", "Sylvain Utard", "Altertable"]
9
+ spec.email = ["contact@altertable.ai"]
10
+
11
+ spec.summary = "A speedy LookML parser and serializer implemented in pure Ruby."
12
+ spec.description = <<~DESC
13
+ Parse LookML strings into Ruby hashes and serialize Ruby hashes back to LookML.
14
+ Implementation uses the same algorithm as the original Python lkml package.
15
+ DESC
16
+ spec.homepage = "https://github.com/altertable-ai/lkml"
17
+ spec.license = "MIT"
18
+ spec.required_ruby_version = ">= 3.3.0"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["rubygems_mfa_required"] = "true"
23
+
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|\.git)/}) }
26
+ end
27
+ spec.bindir = "bin"
28
+ spec.executables = ["lkml"]
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_development_dependency "logger"
32
+ spec.add_development_dependency "minitest", "~> 5.25"
33
+ spec.add_development_dependency "rake", "~> 13.2"
34
+ spec.add_development_dependency "rubocop", "~> 1.69"
35
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Rough CPU-time benchmark over bundled GitHub LookML samples (same idea as scripts/benchmark.py).
5
+
6
+ require_relative "../lib/lkml"
7
+
8
+ dir = File.expand_path("../test/resources/github", __dir__)
9
+ times = []
10
+
11
+ Dir.glob(File.join(dir, "*.lkml")).each do |path|
12
+ text = File.read(path, encoding: "UTF-8")
13
+ start = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
14
+ begin
15
+ Lkml.load(text)
16
+ rescue SyntaxError, KeyError
17
+ next
18
+ end
19
+ finish = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
20
+ times << (finish - start)
21
+ end
22
+
23
+ if times.empty?
24
+ warn "No successful parses; nothing to measure."
25
+ exit 1
26
+ end
27
+
28
+ avg = times.sum / times.size
29
+ variance = times.sum { |t| (t - avg)**2 } / (times.size - 1)
30
+ stdev = Math.sqrt(variance)
31
+
32
+ printf(
33
+ "Average process execution time: %.1f +/- %.1f ms\n",
34
+ avg * 100,
35
+ stdev * 100
36
+ )
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Downloads sample LookML via GitHub Code Search API (same workflow as scripts/download_lookml.py).
5
+ # Requires GITHUB_USERNAME and GITHUB_PERSONAL_ACCESS_TOKEN in the environment.
6
+ # Writes files under test/resources/github/ (create the directory if needed).
7
+
8
+ require "base64"
9
+ require "fileutils"
10
+ require "json"
11
+ require "net/http"
12
+ require "uri"
13
+
14
+ def parse_next_link(link_header)
15
+ return nil if link_header.nil? || link_header.empty?
16
+
17
+ link_header.split(",").each do |segment|
18
+ next unless segment.include?('rel="next"')
19
+
20
+ return Regexp.last_match(1) if segment =~ /<([^>]+)>/
21
+ end
22
+ nil
23
+ end
24
+
25
+ username = ENV.fetch("GITHUB_USERNAME")
26
+ token = ENV.fetch("GITHUB_PERSONAL_ACCESS_TOKEN")
27
+
28
+ directory = File.expand_path("../test/resources/github", __dir__)
29
+ FileUtils.mkdir_p(directory)
30
+
31
+ start_url = "https://api.github.com/search/code?q=view+language:lookml"
32
+ next_url = nil
33
+ page = 1
34
+
35
+ loop do
36
+ uri = URI(next_url || start_url)
37
+ req = Net::HTTP::Get.new(uri)
38
+ req.basic_auth(username, token)
39
+
40
+ res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
41
+ res.value
42
+
43
+ next_url = parse_next_link(res["Link"])
44
+
45
+ warn next_url.inspect
46
+
47
+ payload = JSON.parse(res.body)
48
+ urls = payload.fetch("items").map { |item| item.fetch("url") }
49
+
50
+ warn "Downloading all content from page #{page}"
51
+ urls.each do |url|
52
+ u = URI(url)
53
+ r = Net::HTTP::Get.new(u)
54
+ r.basic_auth(username, token)
55
+ file_res = Net::HTTP.start(u.host, u.port, use_ssl: true) { |h| h.request(r) }
56
+ file_res.value
57
+ body = JSON.parse(file_res.body)
58
+ name = body.fetch("name")
59
+ encoded = body.fetch("content")
60
+ content = Base64.decode64(encoded.gsub("\n", "")).force_encoding("UTF-8")
61
+
62
+ next if name.end_with?(".lookml") || content.start_with?("-") || content.include?("- view")
63
+
64
+ File.write(File.join(directory, name), content)
65
+ end
66
+
67
+ break if next_url.nil?
68
+
69
+ page += 1
70
+ end
metadata CHANGED
@@ -1,38 +1,114 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lkml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
+ - Josh Temple
7
8
  - Sylvain Utard
8
- autorequire:
9
+ - Altertable
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2025-01-19 00:00:00.000000000 Z
12
- dependencies: []
13
- description: ''
14
- email: sylvain.utard@gmail.com
15
- executables: []
12
+ date: 1980-01-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: logger
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: minitest
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '5.25'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '5.25'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '13.2'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '13.2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rubocop
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '1.69'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '1.69'
70
+ description: |
71
+ Parse LookML strings into Ruby hashes and serialize Ruby hashes back to LookML.
72
+ Implementation uses the same algorithm as the original Python lkml package.
73
+ email:
74
+ - contact@altertable.ai
75
+ executables:
76
+ - lkml
16
77
  extensions: []
17
78
  extra_rdoc_files: []
18
79
  files:
80
+ - ".github/workflows/ci.yml"
81
+ - ".gitignore"
82
+ - ".rubocop.yml"
83
+ - ".ruby-version"
84
+ - CONTRIBUTING.md
85
+ - Gemfile
86
+ - Gemfile.lock
19
87
  - LICENSE.md
20
88
  - README.md
89
+ - Rakefile
90
+ - bin/lkml
21
91
  - lib/lkml.rb
92
+ - lib/lkml/cli.rb
22
93
  - lib/lkml/keys.rb
23
94
  - lib/lkml/lexer.rb
24
95
  - lib/lkml/parser.rb
25
96
  - lib/lkml/simple.rb
26
97
  - lib/lkml/tokens.rb
27
98
  - lib/lkml/tree.rb
99
+ - lib/lkml/utils.rb
28
100
  - lib/lkml/version.rb
29
101
  - lib/lkml/visitors.rb
30
- homepage: ''
102
+ - lkml.gemspec
103
+ - script/benchmark.rb
104
+ - script/download_lookml.rb
105
+ homepage: https://github.com/altertable-ai/lkml
31
106
  licenses:
32
107
  - MIT
33
108
  metadata:
109
+ homepage_uri: https://github.com/altertable-ai/lkml
110
+ source_code_uri: https://github.com/altertable-ai/lkml
34
111
  rubygems_mfa_required: 'true'
35
- post_install_message:
36
112
  rdoc_options: []
37
113
  require_paths:
38
114
  - lib
@@ -40,15 +116,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
40
116
  requirements:
41
117
  - - ">="
42
118
  - !ruby/object:Gem::Version
43
- version: '3.2'
119
+ version: 3.3.0
44
120
  required_rubygems_version: !ruby/object:Gem::Requirement
45
121
  requirements:
46
122
  - - ">="
47
123
  - !ruby/object:Gem::Version
48
124
  version: '0'
49
125
  requirements: []
50
- rubygems_version: 3.5.11
51
- signing_key:
126
+ rubygems_version: 3.6.7
52
127
  specification_version: 4
53
- summary: LookML Ruby Parser
128
+ summary: A speedy LookML parser and serializer implemented in pure Ruby.
54
129
  test_files: []