jeny 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![](https://travis-ci.com/enspirit/jeny.svg?branch=master)
|
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: []
|