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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d77c017023caf25f3f6acadeff2cdc098d1ddbb6c75b8ca9f0ac7968abcefef
4
- data.tar.gz: 6613bd618a1793463394f7e4b0408eb30467fb435da58735d61b51fc00d1a22e
3
+ metadata.gz: 947467843616af2d9823cae9e566dd7c7125f7d5eed824b00f23593d05c9874b
4
+ data.tar.gz: 17aca72ede33a42f05be5ce902db30a755842bb19eb51dddc6ef0cb7bbf19016
5
5
  SHA512:
6
- metadata.gz: 668606773d646b80de95eef27aa09d060018a6b5018c4c7d2982e8ad23bc13257437272af13b5284b55d72f2c6ef2f72ff99fdf56e610aad07b87087524ef037
7
- data.tar.gz: dc231523581f3a6125b9a94af667adeb2c6c4211143f8c614655b2da42b3efff6e23cc2b18df05dfc8c9ceacb8768a4a66b1309e3181454c65a8e07f0df71747
6
+ metadata.gz: 10ac65ed0b899f40360bf307b92fb7521d41cb53eeeac34f5b2b9293b91cd07c950251a1288f21c76b0aa6a5d6cb19cbc1b69d9b78a54fd7713e72533124d733
7
+ data.tar.gz: 48de5ca911b5344afd31d31d6c469e7b333b269b572b88a0a740d27133f35f26f93523866a6f912e2728a88969cf1383b08fc0ac88d1e2a6b23e01b90f87db41
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ ruby: [ "3.3", "3.4", "4.0" ]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby }}
20
+ bundler-cache: true
21
+
22
+ - name: Lint with RuboCop
23
+ run: bundle exec rubocop
24
+
25
+ - name: Run tests
26
+ run: bundle exec rake test
27
+ env:
28
+ LKML_RUN_GITHUB_TESTS: 1
data/.gitignore ADDED
@@ -0,0 +1,57 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+ .rspec_status
13
+
14
+ # Used by dotenv library to load environment variables.
15
+ .env
16
+
17
+ # Ignore Byebug command history file.
18
+ .byebug_history
19
+
20
+ ## Specific to RubyMotion:
21
+ .dat*
22
+ .repl_history
23
+ build/
24
+ *.bridgesupport
25
+ build-iPhoneOS/
26
+ build-iPhoneSimulator/
27
+
28
+ ## Specific to RubyMotion (use of CocoaPods):
29
+ #
30
+ # We recommend against adding the Pods directory to your .gitignore. However
31
+ # you should judge for yourself, the pros and cons are mentioned at:
32
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
33
+ #
34
+ # vendor/Pods/
35
+
36
+ ## Documentation cache and generated files:
37
+ /.yardoc/
38
+ /_yardoc/
39
+ /doc/
40
+ /rdoc/
41
+ /docs/api/
42
+
43
+ ## Environment normalization:
44
+ /.bundle/
45
+ /vendor/bundle
46
+ /lib/bundler/man/
47
+
48
+ # for a library or gem, you might want to ignore these files since the code is
49
+ # intended to run in multiple environments; otherwise, check them in:
50
+ Gemfile.lock
51
+ gemfiles/*.lock
52
+ # .ruby-version
53
+ # .ruby-gemset
54
+
55
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
56
+ .rvmrc
57
+
data/.rubocop.yml ADDED
@@ -0,0 +1,25 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.3
4
+ SuggestExtensions: false
5
+
6
+ # Library code does not use RDoc-style class/module comments.
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ # Annotated format tokens add noise for debug logging and printf-style benchmarks.
11
+ Style/FormatStringToken:
12
+ Enabled: false
13
+
14
+ # Dev dependencies are declared in the gemspec for this gem.
15
+ Gemspec/DevelopmentDependencies:
16
+ Enabled: false
17
+
18
+ Style/StringLiterals:
19
+ EnforcedStyle: double_quotes
20
+
21
+ Layout/LineLength:
22
+ Max: 120
23
+
24
+ Metrics:
25
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.6
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,56 @@
1
+ # Contributing to lkml
2
+
3
+ ## Local setup
4
+
5
+ - Fork the repository and clone your fork
6
+ - Create a branch from the default branch (e.g. `feature/your-change`)
7
+ - Install dependencies:
8
+
9
+ ```bash
10
+ bundle install
11
+ ```
12
+
13
+ ## Tests
14
+
15
+ Run the full suite:
16
+
17
+ ```bash
18
+ bundle exec rake test
19
+ ```
20
+
21
+ Add or update tests under `test/` using Minitest (`test/test_*.rb`). Keep fixtures in `test/resources/` as needed.
22
+
23
+ ### GitHub sample acceptance tests
24
+
25
+ A large set of LookML files lives under `test/resources/github/`. Tests in `test/test_github.rb` are **skipped by default**. Enable them with:
26
+
27
+ ```bash
28
+ LKML_RUN_GITHUB_TESTS=1 bundle exec ruby -Itest test/test_github.rb
29
+ ```
30
+
31
+ ### Downloading fresh samples from GitHub
32
+
33
+ `script/download_lookml.rb` queries the GitHub API. Set:
34
+
35
+ - `GITHUB_USERNAME`
36
+ - `GITHUB_PERSONAL_ACCESS_TOKEN`
37
+
38
+ Then:
39
+
40
+ ```bash
41
+ ruby script/download_lookml.rb
42
+ ```
43
+
44
+ Files are written to `test/resources/github/`.
45
+
46
+ ## Style
47
+
48
+ RuboCop is configured for this repo:
49
+
50
+ ```bash
51
+ bundle exec rubocop
52
+ ```
53
+
54
+ ## Pull requests
55
+
56
+ Open a PR against the main repository’s default branch. Reference related issues; use closing keywords (e.g. `Closes #19`) when appropriate.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,76 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lkml (1.2.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.3)
10
+ json (2.19.4)
11
+ language_server-protocol (3.17.0.5)
12
+ lint_roller (1.1.0)
13
+ logger (1.7.0)
14
+ minitest (5.27.0)
15
+ parallel (2.0.1)
16
+ parser (3.3.11.1)
17
+ ast (~> 2.4.1)
18
+ racc
19
+ prism (1.9.0)
20
+ racc (1.8.1)
21
+ rainbow (3.1.1)
22
+ rake (13.4.2)
23
+ regexp_parser (2.12.0)
24
+ rubocop (1.86.1)
25
+ json (~> 2.3)
26
+ language_server-protocol (~> 3.17.0.2)
27
+ lint_roller (~> 1.1.0)
28
+ parallel (>= 1.10)
29
+ parser (>= 3.3.0.2)
30
+ rainbow (>= 2.2.2, < 4.0)
31
+ regexp_parser (>= 2.9.3, < 3.0)
32
+ rubocop-ast (>= 1.49.0, < 2.0)
33
+ ruby-progressbar (~> 1.7)
34
+ unicode-display_width (>= 2.4.0, < 4.0)
35
+ rubocop-ast (1.49.1)
36
+ parser (>= 3.3.7.2)
37
+ prism (~> 1.7)
38
+ ruby-progressbar (1.13.0)
39
+ unicode-display_width (3.2.0)
40
+ unicode-emoji (~> 4.1)
41
+ unicode-emoji (4.2.0)
42
+
43
+ PLATFORMS
44
+ arm64-darwin-25
45
+ ruby
46
+
47
+ DEPENDENCIES
48
+ lkml!
49
+ logger
50
+ minitest (~> 5.25)
51
+ rake (~> 13.2)
52
+ rubocop (~> 1.69)
53
+
54
+ CHECKSUMS
55
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
56
+ json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
57
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
58
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
59
+ lkml (1.2.0)
60
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
61
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
62
+ parallel (2.0.1) sha256=337782d3e39f4121e67563bf91dd8ece67f48923d90698614773a0ec9a5b2c7d
63
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
64
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
65
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
66
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
67
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
68
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
69
+ rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
70
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
71
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
72
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
73
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
74
+
75
+ BUNDLED WITH
76
+ 4.0.7
data/LICENSE.md CHANGED
@@ -1,7 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2019 Josh Temple
4
- Copyright (c) 2025 Sylvain Utard
3
+ Copyright (c) 2019 Josh Temple, 2026 Altertable
5
4
 
6
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7
6
 
data/README.md CHANGED
@@ -1,11 +1,85 @@
1
- # Lkml
1
+ # lkml
2
2
 
3
- A LookML parser and serializer implemented in pure Ruby.
3
+ A fast LookML parser and serializer implemented in **pure Ruby** (stdlib only).
4
4
 
5
- > This is a Ruby rewrite of the amazing [joshtemple/lkml](https://github.com/joshtemple/lkml) python library.
5
+ `Lkml.load` parses LookML text into Ruby hashes (with string keys, suitable for JSON). `Lkml.dump` / `Lkml.generate` serialize hashes back to LookML.
6
6
 
7
- Why should you use `lkml`?
7
+ Why use **lkml**?
8
8
 
9
- - Tested on **over 160K lines of LookML** from public repositories on GitHub
10
- - Written in pure, modern Ruby with **no external dependencies**
11
- - A **full unit test suite** with excellent coverage
9
+ - Exercised on a large corpus of real-world LookML (including bundled GitHub samples)
10
+ - Parses typical view/model files in a few milliseconds (CPU time, excluding I/O)
11
+ - No runtime gem dependencies
12
+ - Full Minitest suite including lexer, parser, round-trip, and CLI checks
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ gem install lkml
18
+ ```
19
+
20
+ Or add to your `Gemfile`:
21
+
22
+ ```ruby
23
+ gem "lkml"
24
+ ```
25
+
26
+ Requires Ruby **3.3+**.
27
+
28
+ ## Usage
29
+
30
+ ```ruby
31
+ require "lkml"
32
+
33
+ hash = Lkml.load('view: orders { sql_table_name: public.orders ;; }')
34
+ # => {"views"=>[{"name"=>"orders", "sql_table_name"=>"public.orders"}]}
35
+
36
+ lookml = Lkml.dump(hash)
37
+ # or
38
+ lookml = Lkml.generate(hash)
39
+ ```
40
+
41
+ Parse to the concrete syntax tree instead of a hash:
42
+
43
+ ```ruby
44
+ tree = Lkml.parse("view: x { dimension: id { type: number sql: ${TABLE}.id ;; } }")
45
+ tree.to_s # round-trip string
46
+ ```
47
+
48
+ ### CLI
49
+
50
+ ```bash
51
+ lkml path/to/file.view.lkml
52
+ ```
53
+
54
+ Pretty-prints JSON.
55
+
56
+ Options:
57
+
58
+ - `-v` / `--verbose` — debug logging on stderr
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ bundle install
64
+ bundle exec rake test
65
+ ```
66
+
67
+ Optional acceptance run over bundled GitHub samples (slow):
68
+
69
+ ```bash
70
+ LKML_RUN_GITHUB_TESTS=1 bundle exec ruby -Itest test/test_github.rb
71
+ ```
72
+
73
+ Benchmark (CPU time over `test/resources/github/*.lkml`):
74
+
75
+ ```bash
76
+ ruby script/benchmark.rb
77
+ ```
78
+
79
+ ## Contributing
80
+
81
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
82
+
83
+ ## License
84
+
85
+ MIT — see [LICENSE.md](LICENSE.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task default: :test
data/bin/lkml ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/lkml/cli"
5
+
6
+ Lkml::CLI.run(ARGV)
data/lib/lkml/cli.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logger"
5
+ require "optparse"
6
+
7
+ require_relative "../lkml"
8
+
9
+ module Lkml
10
+ module CLI
11
+ module_function
12
+
13
+ def parse_args(argv)
14
+ options = { log_level: Logger::WARN }
15
+ parser = OptionParser.new do |opts|
16
+ opts.banner = "usage: lkml [-v|--verbose] FILE"
17
+
18
+ opts.on("-v", "--verbose", "Increase logging verbosity to debug") do
19
+ options[:log_level] = Logger::DEBUG
20
+ end
21
+ end
22
+ parser.parse!(argv)
23
+
24
+ raise OptionParser::MissingArgument, "FILE is required" if argv.empty?
25
+
26
+ options[:file_path] = argv[0]
27
+ options
28
+ end
29
+
30
+ def run(argv = ARGV)
31
+ logger = Logger.new($stderr)
32
+ logger.level = Logger::WARN
33
+
34
+ opts = parse_args(argv.dup)
35
+
36
+ logger.level = opts[:log_level]
37
+
38
+ File.open(opts[:file_path], "r:UTF-8") do |file|
39
+ result = Lkml.load(file)
40
+ puts JSON.pretty_generate(result)
41
+ end
42
+ end
43
+ end
44
+ end
data/lib/lkml/keys.rb CHANGED
@@ -1,125 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Defines constant sequences of LookML keys and helper methods.
4
-
5
- # These are repeatable keys in LookML that the parser should collapse into a single
6
- # Ruby hash key. For example, LookML can have multiple dimensions, so the parser
7
- # will combine those dimensions into an array of hashes with a top-level key,
8
- # `dimensions`.
3
+ require_relative "tokens"
9
4
 
10
5
  module Lkml
11
- PLURAL_KEYS = %w[
12
- access_filter
13
- access_grant
14
- action
15
- aggregate_table
16
- allowed_value
17
- assert
18
- bind_filters
19
- column
20
- constant
21
- datagroup
22
- remote_dependency
23
- derived_column
24
- dimension
25
- dimension_group
26
- explore
27
- extends
28
- filter
29
- filters
30
- form_param
31
- include
32
- join
33
- link
34
- map_layer
35
- measure
36
- named_value_format
37
- option
38
- override_constant
39
- param
40
- parameter
41
- query
42
- set
43
- sql_step
44
- test
45
- user_attribute_param
46
- view
47
- when
48
- ].freeze
6
+ module Keys
7
+ PLURAL_KEYS = %w[
8
+ access_filter access_grant action aggregate_table allowed_value assert bind_filters
9
+ column constant datagroup remote_dependency local_dependency derived_column dimension
10
+ dimension_group explore extends filter filters form_param include join link map_layer
11
+ measure named_value_format option override_constant param parameter query set sql_step
12
+ test user_attribute_param view when
13
+ ].freeze
14
+
15
+ EXPR_BLOCK_KEYS = %w[
16
+ expression_custom_filter expression html sql_trigger_value sql_table_name
17
+ sql_distinct_key sql_start sql_always_having sql_always_where sql_trigger
18
+ sql_foreign_key sql_where sql_end sql_create sql_latitude sql_longitude sql_step
19
+ sql_on sql sql_preamble
20
+ ].freeze
49
21
 
50
- # These are keys in LookML that should be recognized as expression blocks (end with ;;).
22
+ QUOTED_LITERAL_KEYS = %w[
23
+ label view_label group_label group_item_label suggest_persist_for default_value
24
+ direction value_format name url icon_url form_url default tags value description
25
+ sortkeys indexes partition_keys connection include max_cache_age allowed_values
26
+ timezone persist_for cluster_keys distribution extents_json_url feature_key file
27
+ property_key property_label_key else interval_trigger
28
+ ].freeze
51
29
 
52
- EXPR_BLOCK_KEYS = %w[
53
- expression_custom_filter
54
- expression
55
- html
56
- sql_trigger_value
57
- sql_table_name
58
- sql_distinct_key
59
- sql_start
60
- sql_always_having
61
- sql_always_where
62
- sql_trigger
63
- sql_foreign_key
64
- sql_where
65
- sql_end
66
- sql_create
67
- sql_latitude
68
- sql_longitude
69
- sql_step
70
- sql_on
71
- sql
72
- sql_preamble
73
- ].freeze
30
+ KEYS_WITH_NAME_FIELDS = %w[user_attribute_param param form_param option].freeze
74
31
 
75
- # These are keys that the serializer should quote the value of (e.g. `label: "Label"`).
76
- # An example of an unquoted literal would be `hidden: no`.
32
+ CHARACTER_TO_TOKEN = {
33
+ "\0" => Tokens::StreamEndToken,
34
+ "{" => Tokens::BlockStartToken,
35
+ "}" => Tokens::BlockEndToken,
36
+ "[" => Tokens::ListStartToken,
37
+ "]" => Tokens::ListEndToken,
38
+ "," => Tokens::CommaToken,
39
+ ":" => Tokens::ValueToken,
40
+ ";" => Tokens::ExpressionBlockEndToken
41
+ }.freeze
77
42
 
78
- QUOTED_LITERAL_KEYS = %w[
79
- label
80
- view_label
81
- group_label
82
- group_item_label
83
- suggest_persist_for
84
- default_value
85
- direction
86
- value_format
87
- name
88
- url
89
- icon_url
90
- form_url
91
- default
92
- tags
93
- value
94
- description
95
- sortkeys
96
- indexes
97
- partition_keys
98
- connection
99
- include
100
- max_cache_age
101
- allowed_values
102
- timezone
103
- persist_for
104
- cluster_keys
105
- distribution
106
- extents_json_url
107
- feature_key
108
- file
109
- property_key
110
- property_label_key
111
- else
112
- interval_trigger
113
- ].freeze
43
+ module_function
114
44
 
115
- # These are keys for fields in Looker that have a "name" attribute. Since lkml uses the
116
- # key `name` to represent the name of the field (e.g. for `dimension: dimension_name {`,
117
- # the `name` key would hold the value `dimension_name`.)
45
+ def pluralize(key)
46
+ case key
47
+ when "filters", "bind_filters", "extends"
48
+ "#{key}__all"
49
+ when "query"
50
+ "queries"
51
+ when "remote_dependency"
52
+ "remote_dependencies"
53
+ when "local_dependency"
54
+ "local_dependencies"
55
+ else
56
+ "#{key}s"
57
+ end
58
+ end
118
59
 
119
- KEYS_WITH_NAME_FIELDS = %w[
120
- user_attribute_param
121
- param
122
- form_param
123
- option
124
- ].freeze
60
+ def singularize(key)
61
+ case key
62
+ when "queries"
63
+ "query"
64
+ when "remote_dependencies"
65
+ "remote_dependency"
66
+ when "local_dependencies"
67
+ "local_dependency"
68
+ when /__all\z/
69
+ key[0..-6]
70
+ else
71
+ key.end_with?("s") ? key.sub(/s+\z/, "") : key
72
+ end
73
+ end
74
+ end
125
75
  end