jeny 0.1.0 → 1.0.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 +4 -4
- data/Gemfile +0 -3
- data/README.md +193 -2
- data/bin/jeny +3 -0
- data/lib/jeny.rb +11 -0
- data/lib/jeny/caser.rb +113 -0
- data/lib/jeny/code_block.rb +29 -0
- data/lib/jeny/command.rb +111 -0
- data/lib/jeny/command/generate.rb +35 -0
- data/lib/jeny/command/snippets.rb +70 -0
- data/lib/jeny/command/support.rb +30 -0
- data/lib/jeny/configuration.rb +132 -0
- data/lib/jeny/dialect.rb +12 -0
- data/lib/jeny/file.rb +17 -0
- data/lib/jeny/file/full.rb +30 -0
- data/lib/jeny/file/with_blocks.rb +71 -0
- data/lib/jeny/state_manager.rb +51 -0
- data/lib/jeny/state_manager/git.rb +39 -0
- data/lib/jeny/version.rb +2 -2
- data/tasks/test.rake +8 -1
- metadata +36 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80c63bb3ba6b2c4dbb710eb1b465427e207663c56d6f03872871ceb999fd01b8
|
4
|
+
data.tar.gz: 99188a6695c03b3520c0a8b03b499125ac8ba8ff9989114b8520b29e372b0652
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd518b08c9d0e212dbd185552c8acc3e328c727e4a774198c15b9dfdafef2fa8a80103bac09c3c5432b387560450f01ded4b472667b525953cfffce7afc8694f
|
7
|
+
data.tar.gz: 68caadbe294ce2da79a680ee008cbda7d18b0bd2bb2adce536e04c887a96be04dc3be9b6ef685605196077b379c67d252f160079c3172bbc9a360a715426bdab
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,198 @@
|
|
1
1
|
# Jeny
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+

|
4
|
+
|
5
|
+
Jeny is a simple yet powerful commandline tool for scaffolding new
|
6
|
+
projects and generating code snippets in existing ones.
|
7
|
+
|
8
|
+
```sh
|
9
|
+
jeny --help
|
10
|
+
```
|
11
|
+
|
12
|
+
The outline of this readme is as follows:
|
13
|
+
|
14
|
+
- Two use cases: scaffolding & snippets
|
15
|
+
- The templating language
|
16
|
+
- How do I generate dynamic file names?
|
17
|
+
- Use `--git` for atomic generation of snippets
|
18
|
+
- Configuring `jeny`: env vars, .jeny file and commandline options
|
19
|
+
- Contributing, Licence, etc.
|
20
|
+
|
21
|
+
## Generate a project from a scaffold
|
22
|
+
|
23
|
+
The first use case is the generation of a project structure from
|
24
|
+
an existing scaffold.
|
25
|
+
|
26
|
+
```sh
|
27
|
+
jeny --[no-]edit -d ... -d ... generate path/to/scaffold path/to/target
|
28
|
+
```
|
29
|
+
|
30
|
+
This command will recursively copy and instantiate files and
|
31
|
+
directories from the scaffold folder to the destination folder.
|
32
|
+
Your favorite editor opens on files having at least one TODO.
|
33
|
+
|
34
|
+
File content is generated using a very simple and not that
|
35
|
+
powerful templating language, see sections later. Code snippets
|
36
|
+
of the following section are NOT supported for now.
|
37
|
+
|
38
|
+
## Generate code snippets on an existing project
|
39
|
+
|
40
|
+
The second use case is the generation of code snippets inside
|
41
|
+
existing annotated source code.
|
42
|
+
|
43
|
+
```sh
|
44
|
+
jeny --[no-]edit -d ... -d ... snippet snipname path/to/code
|
45
|
+
```
|
46
|
+
|
47
|
+
Code snippets are commented code blocks prefixed by a jeny delimiter.
|
48
|
+
When executing `jeny s`, all files under `path/to/files` are inspected
|
49
|
+
and jeny code blocks instantiated as uncommented code. Your favorite
|
50
|
+
editor opens on files having at least one TODO.
|
51
|
+
|
52
|
+
For instance, when executing
|
53
|
+
|
54
|
+
```sh
|
55
|
+
jeny -d name:hello snippet method .
|
56
|
+
```
|
57
|
+
|
58
|
+
the following code block...
|
59
|
+
|
60
|
+
```
|
61
|
+
#jeny(method) def ${name}
|
62
|
+
#jeny(method) # TODO
|
63
|
+
#jeny(method) end
|
64
|
+
```
|
65
|
+
|
66
|
+
... will generate the code below in the file where it is found,
|
67
|
+
while preserving the jeny code block for subsequent calls:
|
68
|
+
|
69
|
+
```
|
70
|
+
def hello
|
71
|
+
# TODO
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
The `s[snippet]` command is also able to generate fresh new files,
|
76
|
+
provided they end with `.jeny` and have a clearly snippet identifier
|
77
|
+
as first line. For instance, a file called `test.rb.jeny` will be
|
78
|
+
instantiated when generating a `method` snippet if it contains the
|
79
|
+
following first line:
|
80
|
+
|
81
|
+
```
|
82
|
+
#jeny(method)
|
83
|
+
```
|
84
|
+
|
85
|
+
## Templating language
|
86
|
+
|
87
|
+
The current template language uses [WLang](https://github.com/blambeau/wlang)
|
88
|
+
with a very simple dialect. Jeny only supports simple variable for now. It
|
89
|
+
does not support iterations and conditionals, on intent (?).
|
90
|
+
|
91
|
+
Let's say you specify a `-d op_name:my_method` commandline option, the
|
92
|
+
following casing tags are recognized:
|
93
|
+
|
94
|
+
```
|
95
|
+
${op_name} -> my_method (snake)
|
96
|
+
${opname} -> mymethod (flat)
|
97
|
+
${opName} -> myMethod (camel)
|
98
|
+
${OpName} -> MyMethod (pascal)
|
99
|
+
${OP_NAME} -> MY_METHOD (screaming)
|
100
|
+
${OP NAME} -> MYMETHOD (upper)
|
101
|
+
${OPNAME} -> MYMETHOD (upper flat)
|
102
|
+
${op-name} -> my-method (kebab)
|
103
|
+
${Op-Name} -> My-Method (header)
|
104
|
+
${OP-NAME} -> MY-METHOD (cobol)
|
105
|
+
${OP|NAME} -> MY|METHOD (donner)
|
106
|
+
```
|
107
|
+
|
108
|
+
## Dynamic file names
|
109
|
+
|
110
|
+
If you need generated file name names (and/or ancestor folders), you can use
|
111
|
+
the very same tags as those documented in previous section.
|
112
|
+
|
113
|
+
* In scaffolding mode, all files can have dynamic names.
|
114
|
+
* In snippets mode, only files with a `.jeny` ext can.
|
115
|
+
|
116
|
+
## Atomic snippets generation
|
117
|
+
|
118
|
+
The `--git` commandline option (or equivalent configuration, see section
|
119
|
+
below) can be used to generate snippets in an atomic way.
|
120
|
+
|
121
|
+
```sh
|
122
|
+
jeny --git -d ... s snipname
|
123
|
+
```
|
124
|
+
|
125
|
+
Doing so will:
|
126
|
+
- stash any current change
|
127
|
+
- generate code snippets
|
128
|
+
- commit the result
|
129
|
+
- unstash stashed changes
|
130
|
+
|
131
|
+
If anything fails, all changes are reverted before unstashing.
|
132
|
+
|
133
|
+
Please also check the `--no-stash` and `--no-commit` commandline options.
|
134
|
+
|
135
|
+
**IMPORTANT** You must execute `jeny` with a git project as current folder.
|
136
|
+
|
137
|
+
**IMPORTANT** Please use this option with case, as it is still experimental.
|
138
|
+
|
139
|
+
## Configuration and available options
|
140
|
+
|
141
|
+
Jeny behavior can be tuned in three different ways (by ascending order of
|
142
|
+
priority): environment variables, a .jeny configuration file, and command
|
143
|
+
line options.
|
144
|
+
|
145
|
+
For full details, please check the documentation of `Jeny::Configuration`.
|
146
|
+
|
147
|
+
### Environment variables
|
148
|
+
|
149
|
+
* `JENY_EDITOR`, `GIT_EDITOR`, `EDITOR` are inspected in that order to
|
150
|
+
find which source code editor to use
|
151
|
+
|
152
|
+
* `JENY_STATE_MANAGER=none|git` is used to know which state manager to
|
153
|
+
use. See `Jeny::StateManager`
|
154
|
+
|
155
|
+
### .jeny configuration file
|
156
|
+
|
157
|
+
At start `jeny` looks for a `.jeny` configuration file in the current
|
158
|
+
folder and all ancestors, and uses it to override default configuration
|
159
|
+
infered from environment variables.
|
160
|
+
|
161
|
+
```
|
162
|
+
Jeny::Configuration.new do |c|
|
163
|
+
# c is a Jeny::Configuration instance
|
164
|
+
#
|
165
|
+
# many accessors are available for options
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### Commandline options
|
170
|
+
|
171
|
+
Options passed on the commandline always override the configuration
|
172
|
+
obtained through environment variables and the .jeny file.
|
173
|
+
|
174
|
+
For a full list of options, check the help:
|
175
|
+
|
176
|
+
```
|
177
|
+
jeny --help
|
178
|
+
```
|
179
|
+
|
180
|
+
## Public API
|
181
|
+
|
182
|
+
This library uses SemVer 2.0. Its public API is:
|
183
|
+
|
184
|
+
* The command line tool and its options
|
185
|
+
* The Configuration class powering the .jeny file
|
186
|
+
* All recognized environment variables and their effect on configuration
|
187
|
+
* The syntax and semantics of #jeny blocks and file header
|
188
|
+
* The syntax and semantics of the templating language
|
189
|
+
|
190
|
+
## Contributing
|
191
|
+
|
192
|
+
Use github issues and pull requests for contributions. Please favor pull
|
193
|
+
requests if possible for small changes. Make sure to keep in touch with us
|
194
|
+
before making big changes though, as they might not be aligned with our
|
195
|
+
roadmap, or conflict with open pull requests.
|
5
196
|
|
6
197
|
## Licence
|
7
198
|
|
data/bin/jeny
ADDED
data/lib/jeny.rb
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
require 'path'
|
2
|
+
require 'wlang'
|
3
|
+
require 'ostruct'
|
1
4
|
module Jeny
|
5
|
+
class Error < StandardError; end
|
2
6
|
end
|
3
7
|
require_relative 'jeny/version'
|
8
|
+
require_relative 'jeny/state_manager'
|
9
|
+
require_relative 'jeny/configuration'
|
10
|
+
require_relative 'jeny/caser'
|
11
|
+
require_relative 'jeny/dialect'
|
12
|
+
require_relative 'jeny/file'
|
13
|
+
require_relative 'jeny/code_block'
|
14
|
+
require_relative 'jeny/command'
|
data/lib/jeny/caser.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
module Jeny
|
2
|
+
module Caser
|
3
|
+
|
4
|
+
def self.for_hash(hash)
|
5
|
+
hash.each_pair.each_with_object({}) do |(k,v),memo|
|
6
|
+
case v
|
7
|
+
when String
|
8
|
+
Caser.methods(false).each do |m|
|
9
|
+
next if m == :for_hash or m == :gen_parts
|
10
|
+
memo[Caser.send(m, k)] = Caser.send(m, v)
|
11
|
+
end
|
12
|
+
when Hash
|
13
|
+
memo[k] = for_hash(v)
|
14
|
+
when Array
|
15
|
+
memo[k] = v.map{|x| for_hash(x) }
|
16
|
+
else
|
17
|
+
v
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.gen_parts(src)
|
23
|
+
src.split(/[ -_]/)
|
24
|
+
end
|
25
|
+
|
26
|
+
def flat(src)
|
27
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
28
|
+
parts.join
|
29
|
+
end
|
30
|
+
module_function :flat
|
31
|
+
|
32
|
+
def upper(src)
|
33
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
34
|
+
parts.map(&:upcase).join(" ")
|
35
|
+
end
|
36
|
+
module_function :upper
|
37
|
+
|
38
|
+
def upperflat(src)
|
39
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
40
|
+
parts.map(&:upcase).join
|
41
|
+
end
|
42
|
+
module_function :upperflat
|
43
|
+
|
44
|
+
def screaming(src)
|
45
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
46
|
+
parts.map(&:upcase).join("_")
|
47
|
+
end
|
48
|
+
module_function :screaming
|
49
|
+
|
50
|
+
def macro(src)
|
51
|
+
screaming(src)
|
52
|
+
end
|
53
|
+
module_function :macro
|
54
|
+
|
55
|
+
def constant(src)
|
56
|
+
screaming(src)
|
57
|
+
end
|
58
|
+
module_function :constant
|
59
|
+
|
60
|
+
def underscore(src)
|
61
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
62
|
+
parts.join("_")
|
63
|
+
end
|
64
|
+
module_function :underscore
|
65
|
+
|
66
|
+
def snake(src)
|
67
|
+
underscore(src)
|
68
|
+
end
|
69
|
+
module_function :snake
|
70
|
+
|
71
|
+
def camel(src)
|
72
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
73
|
+
parts.first + parts[1..-1].map(&:capitalize).join
|
74
|
+
end
|
75
|
+
module_function :camel
|
76
|
+
|
77
|
+
def pascal(src)
|
78
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
79
|
+
parts.map(&:capitalize).join
|
80
|
+
end
|
81
|
+
module_function :pascal
|
82
|
+
|
83
|
+
def kebab(src)
|
84
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
85
|
+
parts.join("-")
|
86
|
+
end
|
87
|
+
module_function :kebab
|
88
|
+
|
89
|
+
def train(src)
|
90
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
91
|
+
parts.map(&:capitalize).join("-")
|
92
|
+
end
|
93
|
+
module_function :train
|
94
|
+
|
95
|
+
def header(src)
|
96
|
+
train(src)
|
97
|
+
end
|
98
|
+
module_function :header
|
99
|
+
|
100
|
+
def cobol(src)
|
101
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
102
|
+
parts.map(&:upcase).join("-")
|
103
|
+
end
|
104
|
+
module_function :cobol
|
105
|
+
|
106
|
+
def donner(src)
|
107
|
+
parts = gen_parts(src) unless src.is_a?(Array)
|
108
|
+
parts.join("|")
|
109
|
+
end
|
110
|
+
module_function :donner
|
111
|
+
|
112
|
+
end # module Caser
|
113
|
+
end # module Jeny
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Jeny
|
2
|
+
class CodeBlock
|
3
|
+
|
4
|
+
def initialize(source, path, line, asset)
|
5
|
+
@source = source
|
6
|
+
@path = path
|
7
|
+
@line = line
|
8
|
+
@asset = asset
|
9
|
+
end
|
10
|
+
attr_reader :source, :path, :line, :asset
|
11
|
+
|
12
|
+
def line_index
|
13
|
+
line - 1
|
14
|
+
end
|
15
|
+
|
16
|
+
def instantiate(data)
|
17
|
+
case d = data[asset]
|
18
|
+
when NilClass
|
19
|
+
when Hash
|
20
|
+
Dialect.render(source, d)
|
21
|
+
when Array
|
22
|
+
d.map{|item| instantiate(asset => item) }.join("\n")
|
23
|
+
else
|
24
|
+
raise Error, "Unexpected block asset: `#{asset} = #{d}`"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class CodeBlock
|
29
|
+
end # module Jeny
|
data/lib/jeny/command.rb
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
module Jeny
|
3
|
+
class Command
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@config = nil
|
7
|
+
@jeny_data = {}
|
8
|
+
end
|
9
|
+
attr_reader :jeny_data
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
def self.call(argv)
|
13
|
+
new.call(argv)
|
14
|
+
rescue Error => ex
|
15
|
+
puts ex.message
|
16
|
+
exit(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(argv)
|
20
|
+
args = parse_argv!(argv, load_config!)
|
21
|
+
case command = args.first
|
22
|
+
when "g", "generate"
|
23
|
+
_, from, to = args
|
24
|
+
from, to = Path(from), Path(to)
|
25
|
+
raise Error, "No such template `#{from}`" unless from.directory?
|
26
|
+
to.mkdir_p
|
27
|
+
Generate.new(@config, jeny_data, from, to).call
|
28
|
+
when "s", "snippets"
|
29
|
+
_, asset, source = args
|
30
|
+
raise Error, "Asset must be specified" if asset.nil? or Path(asset).exist?
|
31
|
+
source ||= Path.pwd
|
32
|
+
Snippets.new(@config, jeny_data, asset, Path(source)).call
|
33
|
+
else
|
34
|
+
raise Error, "Unknown command `#{command}`"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_argv!(argv, config = Configuration.new)
|
39
|
+
@config = config
|
40
|
+
option_parser(config).parse!(argv)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def load_config!
|
46
|
+
jeny_file = Path.pwd/".jeny"
|
47
|
+
jeny_file = Path.backfind(".jeny") unless jeny_file.file?
|
48
|
+
unless jeny_file
|
49
|
+
puts "Using default Jeny configuration"
|
50
|
+
return Configuration.new
|
51
|
+
end
|
52
|
+
unless (cf = Path(jeny_file)).file?
|
53
|
+
raise Error, "No such file `#{jeny_file}`"
|
54
|
+
end
|
55
|
+
unless (config = Kernel.eval(cf.read)).is_a?(Configuration)
|
56
|
+
raise Error, "Config file corrupted, no Configuration returned"
|
57
|
+
end
|
58
|
+
puts "Using #{jeny_file}"
|
59
|
+
config.tap{|c|
|
60
|
+
c.jeny_file = cf
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def option_parser(config)
|
65
|
+
option_parser ||= OptionParser.new do |opts|
|
66
|
+
opts.banner = <<~B
|
67
|
+
Usage: jeny [options] g[enerate] SOURCE TARGET
|
68
|
+
jeny [options] s[nippets] ASSET [TARGET]
|
69
|
+
B
|
70
|
+
opts.on("-d key:value", "Add generation data") do |pair|
|
71
|
+
k, v = pair.split(':')
|
72
|
+
@jeny_data[k] = v
|
73
|
+
end
|
74
|
+
opts.on("-f datafile", "Take generation data from a file") do |file|
|
75
|
+
file = Path(file)
|
76
|
+
raise Error, "No such file: #{file}" unless file.exists?
|
77
|
+
require 'yaml' if file.ext == 'yml' || file.ext == 'yaml'
|
78
|
+
require 'json' if file.ext == 'json'
|
79
|
+
@jeny_data = file.load
|
80
|
+
end
|
81
|
+
opts.on("--git", "Use git as state manager") do
|
82
|
+
config.state_manager = :git
|
83
|
+
end
|
84
|
+
opts.on("--edit-if=MATCH", "Edit files matching a given term") do |s|
|
85
|
+
config.edit_changed_files = ->(f,c){ c =~ Regexp.new(s) }
|
86
|
+
end
|
87
|
+
opts.on("--[no-]edit", "Edit files having a TODO") do |s|
|
88
|
+
config.edit_changed_files = s ? Configuration::DEFAULT_EDIT_PROC : false
|
89
|
+
end
|
90
|
+
opts.on("--[no-]stash", "Stash before generating snippets") do |s|
|
91
|
+
config.state_manager_options[:stash] = s
|
92
|
+
end
|
93
|
+
opts.on("--[no-]commit", "Commit generated snippets") do |c|
|
94
|
+
config.state_manager_options[:commit] = c
|
95
|
+
end
|
96
|
+
opts.on("-v", "--version", "Prints version") do
|
97
|
+
puts "Jeny v#{VERSION}"
|
98
|
+
exit
|
99
|
+
end
|
100
|
+
opts.on("-h", "--help", "Prints this help") do
|
101
|
+
puts opts
|
102
|
+
exit
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end # class Command
|
108
|
+
end # module Jeny
|
109
|
+
require_relative 'command/support'
|
110
|
+
require_relative 'command/generate'
|
111
|
+
require_relative 'command/snippets'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Jeny
|
2
|
+
class Command
|
3
|
+
class Generate
|
4
|
+
include Support
|
5
|
+
|
6
|
+
def initialize(config, data, from, to)
|
7
|
+
@config = config
|
8
|
+
@data = Caser.for_hash(data)
|
9
|
+
@from = from
|
10
|
+
@to = to
|
11
|
+
end
|
12
|
+
attr_reader :config, :data, :from, :to
|
13
|
+
|
14
|
+
def call
|
15
|
+
puts
|
16
|
+
changed = []
|
17
|
+
from.glob("**/*") do |source|
|
18
|
+
target = target_for(source)
|
19
|
+
puts "creating #{simplify_path(target)}"
|
20
|
+
if source.directory?
|
21
|
+
target.mkdir_p
|
22
|
+
else
|
23
|
+
target.parent.mkdir_p
|
24
|
+
file = File::Full.new(source, config)
|
25
|
+
target_content = file.instantiate(data)
|
26
|
+
target.write(target_content)
|
27
|
+
changed << [target, target_content]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
edit_changed_files(changed)
|
31
|
+
end
|
32
|
+
|
33
|
+
end # class Generate
|
34
|
+
end # class Command
|
35
|
+
end # module Jeny
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Jeny
|
2
|
+
class Command
|
3
|
+
class Snippets
|
4
|
+
include Support
|
5
|
+
|
6
|
+
def initialize(config, data, asset, from)
|
7
|
+
@config = config
|
8
|
+
@data = { asset => Caser.for_hash(data) }
|
9
|
+
@asset = asset
|
10
|
+
@from = from
|
11
|
+
end
|
12
|
+
attr_reader :config, :data, :asset, :from
|
13
|
+
|
14
|
+
alias :to :from
|
15
|
+
|
16
|
+
def call
|
17
|
+
puts
|
18
|
+
sm, state = config.state_manager, OpenStruct.new
|
19
|
+
sm.stash(state) if config.sm_stash?
|
20
|
+
|
21
|
+
changed = []
|
22
|
+
from.glob("**/*").each do |source|
|
23
|
+
next if source.directory?
|
24
|
+
next if config.ignore_file?(source)
|
25
|
+
pair = snippet_it(source)
|
26
|
+
changed << pair if pair
|
27
|
+
end
|
28
|
+
|
29
|
+
sm.commit(changed.map(&:first), state) if config.sm_commit?
|
30
|
+
|
31
|
+
edit_changed_files(changed)
|
32
|
+
rescue
|
33
|
+
sm.reset(changed.map(&:first), state)
|
34
|
+
raise
|
35
|
+
ensure
|
36
|
+
sm.unstash(state) if config.sm_stash?
|
37
|
+
end
|
38
|
+
|
39
|
+
def snippet_it(source)
|
40
|
+
target, target_content = nil
|
41
|
+
if source.ext =~ /\.?jeny/
|
42
|
+
file = File::Full.new(source, config)
|
43
|
+
if file.has_jeny_context?
|
44
|
+
ctx = file.instantiate_context(data)
|
45
|
+
if ctx
|
46
|
+
target_content = file.instantiate(ctx)
|
47
|
+
target = target_for(source, ctx)
|
48
|
+
target.parent.mkdir_p
|
49
|
+
target.write(target_content)
|
50
|
+
puts "snippets #{simplify_path(target)}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
else
|
54
|
+
file = File::WithBlocks.new(source, config)
|
55
|
+
if file.has_jeny_blocks?
|
56
|
+
target_content = file.instantiate(data)
|
57
|
+
target = target_for(source)
|
58
|
+
target.write(target_content)
|
59
|
+
puts "snippets #{simplify_path(target)}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
target ? [target, target_content] : nil
|
63
|
+
rescue => ex
|
64
|
+
msg = "Error in `#{simplify_path(source)}`: #{ex.message}"
|
65
|
+
raise Error, msg
|
66
|
+
end
|
67
|
+
|
68
|
+
end # class Snippets
|
69
|
+
end # class Command
|
70
|
+
end # module Jeny
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Jeny
|
2
|
+
class Command
|
3
|
+
module Support
|
4
|
+
|
5
|
+
def target_for(source, data = self.data)
|
6
|
+
relative = source.relative_to(from)
|
7
|
+
relative.each_filename.map{|f|
|
8
|
+
Dialect.render(f.gsub(/.jeny$/, ""), data)
|
9
|
+
}.inject(to){|t,part| t/part }
|
10
|
+
end
|
11
|
+
|
12
|
+
def simplify_path(path)
|
13
|
+
if path.to_s.start_with?(Path.pwd.to_s)
|
14
|
+
path.relative_to(Path.pwd)
|
15
|
+
else
|
16
|
+
path
|
17
|
+
end
|
18
|
+
end
|
19
|
+
module_function :simplify_path
|
20
|
+
|
21
|
+
def edit_changed_files(changed)
|
22
|
+
to_open = changed
|
23
|
+
.select{|pair| config.should_be_edited?(*pair) }
|
24
|
+
.map{|pair| simplify_path(pair.first) }
|
25
|
+
config.open_editor(to_open) unless to_open.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
end # module Support
|
29
|
+
end # class Command
|
30
|
+
end # module Jeny
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Jeny
|
2
|
+
class Configuration
|
3
|
+
|
4
|
+
DEFAULT_EDIT_PROC = ->(_,content) {
|
5
|
+
content =~ /TODO/
|
6
|
+
}
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@jeny_block_delimiter = "#jeny"
|
10
|
+
@ignore_pattern = /^(vendor|\.bundle)/
|
11
|
+
@editor_command = default_editor_command
|
12
|
+
@edit_changed_files = DEFAULT_EDIT_PROC
|
13
|
+
@state_manager = default_state_manager
|
14
|
+
@state_manager_options = {
|
15
|
+
stash: true,
|
16
|
+
commit: true
|
17
|
+
}
|
18
|
+
yield(self) if block_given?
|
19
|
+
end
|
20
|
+
attr_accessor :jeny_file
|
21
|
+
|
22
|
+
# The delimiter used for jeny block in source code files.
|
23
|
+
#
|
24
|
+
# Defaults to `#jeny`
|
25
|
+
attr_accessor :jeny_block_delimiter
|
26
|
+
|
27
|
+
# Regular expression matching files that can always be ignored by Snippets.
|
28
|
+
#
|
29
|
+
# Defaults to /^(vendor|\.bundle)/
|
30
|
+
attr_accessor :ignore_pattern
|
31
|
+
|
32
|
+
# Shell command to open the source code editor.
|
33
|
+
#
|
34
|
+
# Default value checks the JENY_EDITOR, GIT_EDITOR, EDITOR
|
35
|
+
# environment variables, and fallbacks to "code".
|
36
|
+
attr_accessor :editor_command
|
37
|
+
|
38
|
+
def default_editor_command
|
39
|
+
ENV['JENY_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
|
40
|
+
end
|
41
|
+
|
42
|
+
# State manager to use.
|
43
|
+
#
|
44
|
+
# Default value check the JENY_STATE_MANAGER environment variable:
|
45
|
+
# - `none`, no state management is done
|
46
|
+
# - `git`, git is used to stash/unstash/commit/reset
|
47
|
+
#
|
48
|
+
# Defaults to `none`, that is, to an empty state manager.
|
49
|
+
attr_reader :state_manager
|
50
|
+
|
51
|
+
# Options for the state manager.
|
52
|
+
#
|
53
|
+
# This is a Hash, with `:stash` and `:commit` keys mapping to
|
54
|
+
# either true of false.
|
55
|
+
#
|
56
|
+
# Both are true by default.
|
57
|
+
attr_reader :state_manager_options
|
58
|
+
|
59
|
+
# :nodoc:
|
60
|
+
def sm_stash?
|
61
|
+
state_manager_options[:stash]
|
62
|
+
end
|
63
|
+
|
64
|
+
# :nodoc:
|
65
|
+
def sm_commit?
|
66
|
+
state_manager_options[:commit]
|
67
|
+
end
|
68
|
+
|
69
|
+
# Sets the state manager to use. `sm` can be a state manager instance,
|
70
|
+
# of a string of symbol with same value as JENY_STATE_MANAGER env
|
71
|
+
# variable.
|
72
|
+
def state_manager=(sm)
|
73
|
+
@state_manager = case sm
|
74
|
+
when StateManager then sm
|
75
|
+
when :git, "git" then StateManager::Git.new(self)
|
76
|
+
else StateManager.new(self)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def default_state_manager
|
81
|
+
case ENV['JENY_STATE_MANAGER']
|
82
|
+
when "git"
|
83
|
+
StateManager::Git.new(self)
|
84
|
+
else
|
85
|
+
StateManager.new(self)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Whether files generated/modified must be edited right after.
|
90
|
+
#
|
91
|
+
# Accepted values are:
|
92
|
+
# - `false`, then source code edition is disabled
|
93
|
+
# - `true`, then all files are open after being snipetted
|
94
|
+
# - `/.../`, then only files whose name match the regexp are open
|
95
|
+
# - `->(f,c){}`, the file `f` whose generated content is `c` is open
|
96
|
+
# is the proc returns a truthy value.
|
97
|
+
#
|
98
|
+
# Defaults to a Proc that opens files having at least one TODO.
|
99
|
+
attr_accessor :edit_changed_files
|
100
|
+
|
101
|
+
alias :edit_changed_files? :edit_changed_files
|
102
|
+
|
103
|
+
# Should `file` be ignored?
|
104
|
+
def ignore_file?(file)
|
105
|
+
file = Command::Support.simplify_path(file)
|
106
|
+
file.to_s =~ ignore_pattern
|
107
|
+
end
|
108
|
+
|
109
|
+
# Whether file should be edited after being snippetted
|
110
|
+
def should_be_edited?(file, content)
|
111
|
+
return false if editor_command.nil?
|
112
|
+
case edit_changed_files
|
113
|
+
when false, true, nil
|
114
|
+
!!edit_changed_files
|
115
|
+
when Regexp
|
116
|
+
!!(file.to_s =~ edit_changed_files)
|
117
|
+
when Proc
|
118
|
+
!!edit_changed_files.call(file, content)
|
119
|
+
else
|
120
|
+
raise Error, "Wrong edit_changed_files `#{edit_changed_files}`"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def open_editor(files)
|
125
|
+
unless editor_command
|
126
|
+
raise Error, "source code editor is not enabled"
|
127
|
+
end
|
128
|
+
fork{ exec(editor_command + " " + files.join(" ")) }
|
129
|
+
end
|
130
|
+
|
131
|
+
end # class Configuration
|
132
|
+
end # module Jeny
|
data/lib/jeny/dialect.rb
ADDED
data/lib/jeny/file.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Jeny
|
2
|
+
class File
|
3
|
+
|
4
|
+
def initialize(path, config)
|
5
|
+
@path = path
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
attr_reader :path, :config
|
9
|
+
|
10
|
+
def rewrite(data, to)
|
11
|
+
to.write(instantiate(data))
|
12
|
+
end
|
13
|
+
|
14
|
+
end # class File
|
15
|
+
end # module Jeny
|
16
|
+
require_relative 'file/with_blocks'
|
17
|
+
require_relative 'file/full'
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Jeny
|
2
|
+
class File
|
3
|
+
class Full < File
|
4
|
+
|
5
|
+
def context_rgx
|
6
|
+
/^#{config.jeny_block_delimiter}\(([a-z]+)\)\s*$/
|
7
|
+
end
|
8
|
+
|
9
|
+
def has_jeny_context?
|
10
|
+
path.readlines.first =~ context_rgx
|
11
|
+
end
|
12
|
+
|
13
|
+
def instantiate_context(data)
|
14
|
+
if path.readlines.first =~ context_rgx
|
15
|
+
data[$1]
|
16
|
+
else
|
17
|
+
data
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def instantiate(data)
|
22
|
+
path.readlines.map{|l|
|
23
|
+
next if l =~ context_rgx
|
24
|
+
Dialect.render(l, data)
|
25
|
+
}.compact.join("")
|
26
|
+
end
|
27
|
+
|
28
|
+
end # class Full
|
29
|
+
end # class File
|
30
|
+
end # module Jeny
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Jeny
|
2
|
+
class File
|
3
|
+
class WithBlocks < File
|
4
|
+
|
5
|
+
def has_jeny_blocks?
|
6
|
+
!jeny_blocks.empty?
|
7
|
+
end
|
8
|
+
|
9
|
+
def jeny_blocks
|
10
|
+
@jeny_blocks ||= _parse_jeny_blocks(path.readlines, 0, [])
|
11
|
+
end
|
12
|
+
|
13
|
+
def instantiate(data)
|
14
|
+
return path.read unless has_jeny_blocks?
|
15
|
+
_instantiate(data, path.readlines, 0, jeny_blocks, []).join("")
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def _parse_jeny_blocks(lines, index, blocks)
|
21
|
+
if index >= lines.size
|
22
|
+
blocks
|
23
|
+
elsif lines[index] =~ jeny_block_regex
|
24
|
+
source, new_index = _parse_jeny_block(lines, index)
|
25
|
+
blocks << CodeBlock.new(source, path, index+1, $1)
|
26
|
+
_parse_jeny_blocks(lines, new_index+1, blocks)
|
27
|
+
else
|
28
|
+
_parse_jeny_blocks(lines, index+1, blocks)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def _parse_jeny_block(lines, index, str = "")
|
33
|
+
if lines[index] =~ jeny_block_regex
|
34
|
+
_parse_jeny_block(lines, index+1, str + lines[index])
|
35
|
+
else
|
36
|
+
source = str.split("\n").map{|s|
|
37
|
+
s.gsub(jeny_block_gsub, "")
|
38
|
+
}
|
39
|
+
source = source.size == 1 ? source.first : source.join("\n")+"\n"
|
40
|
+
[source, index]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def _instantiate(data, lines, index, blocks, acc)
|
45
|
+
block = blocks.first
|
46
|
+
if block.nil?
|
47
|
+
lines[index..-1].each{|l| acc << l }
|
48
|
+
acc
|
49
|
+
elsif block.line_index == index
|
50
|
+
if i = block.instantiate(data)
|
51
|
+
acc << i << "\n"
|
52
|
+
end
|
53
|
+
acc << lines[index]
|
54
|
+
_instantiate(data, lines, index+1, blocks[1..-1], acc)
|
55
|
+
else
|
56
|
+
lines[index...block.line_index].each{|l| acc << l }
|
57
|
+
_instantiate(data, lines, block.line_index, blocks, acc)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def jeny_block_regex
|
62
|
+
@jeny_block_regex ||= %r{^\s*#{config.jeny_block_delimiter}\(([a-z]+)\)}m
|
63
|
+
end
|
64
|
+
|
65
|
+
def jeny_block_gsub
|
66
|
+
@jeny_block_gsub ||= %r{#{config.jeny_block_delimiter}(\([a-z]+\))[ ]?}
|
67
|
+
end
|
68
|
+
|
69
|
+
end # class WithBlocks
|
70
|
+
end # class File
|
71
|
+
end # module Jeny
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Jeny
|
2
|
+
# State Manager abstraction used by Snippets to work atomically.
|
3
|
+
#
|
4
|
+
# An implementation is supposed to be stateless (no instance
|
5
|
+
# variables). The methods receive a `state` argument, which is
|
6
|
+
# an OpenStruct that can be used to track state accros calls.
|
7
|
+
#
|
8
|
+
# See StateManager::Git for a typical implementation using git.
|
9
|
+
class StateManager
|
10
|
+
|
11
|
+
def initialize(config)
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
attr_reader :config
|
15
|
+
|
16
|
+
# Make sure the working directory is clean, for instance by stashing
|
17
|
+
# any pending change.
|
18
|
+
#
|
19
|
+
# The method MAY raise an Error if the current state is not clean and
|
20
|
+
# nothing can be done about it.
|
21
|
+
#
|
22
|
+
# The method SHOULD NOT raise an Error is nothing needs to be done.
|
23
|
+
def stash(state)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Unstash changes stashed the last time `stash` has been called.
|
27
|
+
#
|
28
|
+
# This method MAY NOT raise errors since a previous `stash`
|
29
|
+
# has been successfuly done earlier.
|
30
|
+
#
|
31
|
+
# The method SHOULD NOT raise an Error is nothing needs to be done.
|
32
|
+
def unstash(state)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Reset all changes to files in `changed`, typically because an error
|
36
|
+
# occured.
|
37
|
+
#
|
38
|
+
# The method SHOULD NOT raise an Error in any case.
|
39
|
+
def reset(changed, state)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Commit all changes to the files in `changed`.
|
43
|
+
#
|
44
|
+
# The method MAY raise an Error, but it will force jeny to reset
|
45
|
+
# everything.
|
46
|
+
def commit(changed, state)
|
47
|
+
end
|
48
|
+
|
49
|
+
end # class StateManage
|
50
|
+
end # module Jeny
|
51
|
+
require_relative 'state_manager/git'
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Jeny
|
2
|
+
class StateManager
|
3
|
+
# StateManager implementation that uses git to manage state management.
|
4
|
+
#
|
5
|
+
# This state management requires executing jeny in the git root folder.
|
6
|
+
class Git < StateManager
|
7
|
+
|
8
|
+
# Executes a `git stash`
|
9
|
+
def stash(state)
|
10
|
+
system("git diff --exit-code")
|
11
|
+
state.stashed = ($?.exitstatus != 0)
|
12
|
+
system("git stash") if state.stashed
|
13
|
+
end
|
14
|
+
|
15
|
+
# Executes a `git stash pop`
|
16
|
+
def unstash(state)
|
17
|
+
system("git stash pop") if state.stashed
|
18
|
+
end
|
19
|
+
|
20
|
+
# Reset all changes through a `git reset --hard`.
|
21
|
+
#
|
22
|
+
# WARN: changes not related to `changed` are reverted too,
|
23
|
+
# which should be nothing since a stash has been done before.
|
24
|
+
def reset(changed, state)
|
25
|
+
changed.each{|f| f.rm_rf if f.exists? }
|
26
|
+
system("git reset --hard")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Commits all changes, using `git add` the `git commit`
|
30
|
+
def commit(changed, state)
|
31
|
+
return if changed.empty?
|
32
|
+
system("git add #{changed.join(" ")}")
|
33
|
+
msg = "jeny #{$*.join(' ')}"
|
34
|
+
system("git commit -m '#{msg}'")
|
35
|
+
end
|
36
|
+
|
37
|
+
end # class Git
|
38
|
+
end # class StateManager
|
39
|
+
end # module Jeny
|
data/lib/jeny/version.rb
CHANGED
data/tasks/test.rake
CHANGED
@@ -6,10 +6,17 @@ namespace :test do
|
|
6
6
|
desc "Runs unit tests"
|
7
7
|
RSpec::Core::RakeTask.new(:unit) do |t|
|
8
8
|
t.pattern = "spec/unit/**/test_*.rb"
|
9
|
-
t.rspec_opts = ["-Ilib", "-Ispec
|
9
|
+
t.rspec_opts = ["-Ilib", "-Ispec", "--color", "--backtrace", "--format=progress"]
|
10
10
|
end
|
11
11
|
tests << :unit
|
12
12
|
|
13
|
+
desc "Runs command tests"
|
14
|
+
RSpec::Core::RakeTask.new(:command) do |t|
|
15
|
+
t.pattern = "spec/command/**/test_*.rb"
|
16
|
+
t.rspec_opts = ["-Ilib", "-Ispec", "--color", "--backtrace", "--format=progress"]
|
17
|
+
end
|
18
|
+
tests << :command
|
19
|
+
|
13
20
|
task :all => tests
|
14
21
|
end
|
15
22
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jeny
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bernard Lambeau
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-12-
|
11
|
+
date: 2020-12-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: path
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: wlang
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rake
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -54,7 +68,8 @@ dependencies:
|
|
54
68
|
version: '3.7'
|
55
69
|
description: Simple scaffolding and code generation
|
56
70
|
email: blambeau@gmail.com
|
57
|
-
executables:
|
71
|
+
executables:
|
72
|
+
- jeny
|
58
73
|
extensions: []
|
59
74
|
extra_rdoc_files: []
|
60
75
|
files:
|
@@ -62,7 +77,21 @@ files:
|
|
62
77
|
- LICENSE.md
|
63
78
|
- README.md
|
64
79
|
- Rakefile
|
80
|
+
- bin/jeny
|
65
81
|
- lib/jeny.rb
|
82
|
+
- lib/jeny/caser.rb
|
83
|
+
- lib/jeny/code_block.rb
|
84
|
+
- lib/jeny/command.rb
|
85
|
+
- lib/jeny/command/generate.rb
|
86
|
+
- lib/jeny/command/snippets.rb
|
87
|
+
- lib/jeny/command/support.rb
|
88
|
+
- lib/jeny/configuration.rb
|
89
|
+
- lib/jeny/dialect.rb
|
90
|
+
- lib/jeny/file.rb
|
91
|
+
- lib/jeny/file/full.rb
|
92
|
+
- lib/jeny/file/with_blocks.rb
|
93
|
+
- lib/jeny/state_manager.rb
|
94
|
+
- lib/jeny/state_manager/git.rb
|
66
95
|
- lib/jeny/version.rb
|
67
96
|
- tasks/gem.rake
|
68
97
|
- tasks/test.rake
|
@@ -70,7 +99,7 @@ homepage: http://github.com/enspirit/jeny
|
|
70
99
|
licenses:
|
71
100
|
- MIT
|
72
101
|
metadata: {}
|
73
|
-
post_install_message:
|
102
|
+
post_install_message:
|
74
103
|
rdoc_options: []
|
75
104
|
require_paths:
|
76
105
|
- lib
|
@@ -85,8 +114,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
114
|
- !ruby/object:Gem::Version
|
86
115
|
version: '0'
|
87
116
|
requirements: []
|
88
|
-
rubygems_version: 3.
|
89
|
-
signing_key:
|
117
|
+
rubygems_version: 3.1.4
|
118
|
+
signing_key:
|
90
119
|
specification_version: 4
|
91
120
|
summary: Simple scaffolding and code generation
|
92
121
|
test_files: []
|