fir-cli-x 1.7.2.1
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 +7 -0
- data/.codeclimate.yml +8 -0
- data/.dockerignore +2 -0
- data/.flow-plugin.yml +14 -0
- data/.gitignore +27 -0
- data/.travis.yml +23 -0
- data/CHANGELOG +194 -0
- data/Dockerfile +12 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +63 -0
- data/Rakefile +10 -0
- data/bin/console +11 -0
- data/bin/fir +14 -0
- data/bin/setup +7 -0
- data/doc/help.md +34 -0
- data/doc/info.md +44 -0
- data/doc/install.md +67 -0
- data/doc/login.md +19 -0
- data/doc/publish.md +35 -0
- data/doc/upgrade.md +7 -0
- data/fir-cli.gemspec +52 -0
- data/fir.sh +46 -0
- data/install.sh +210 -0
- data/lib/fir-cli.rb +3 -0
- data/lib/fir.rb +28 -0
- data/lib/fir/api.yml +7 -0
- data/lib/fir/cli.rb +181 -0
- data/lib/fir/patches.rb +10 -0
- data/lib/fir/patches/blank.rb +131 -0
- data/lib/fir/patches/concern.rb +146 -0
- data/lib/fir/patches/default_headers.rb +9 -0
- data/lib/fir/patches/hash.rb +79 -0
- data/lib/fir/patches/instance_variables.rb +30 -0
- data/lib/fir/patches/native_patch.rb +28 -0
- data/lib/fir/patches/os_patch.rb +28 -0
- data/lib/fir/patches/try.rb +102 -0
- data/lib/fir/util.rb +86 -0
- data/lib/fir/util/build_apk.rb +77 -0
- data/lib/fir/util/build_common.rb +93 -0
- data/lib/fir/util/build_ipa.rb +11 -0
- data/lib/fir/util/config.rb +43 -0
- data/lib/fir/util/http.rb +23 -0
- data/lib/fir/util/info.rb +38 -0
- data/lib/fir/util/login.rb +17 -0
- data/lib/fir/util/mapping.rb +98 -0
- data/lib/fir/util/me.rb +19 -0
- data/lib/fir/util/parser/apk.rb +46 -0
- data/lib/fir/util/parser/bin/pngcrush +0 -0
- data/lib/fir/util/parser/common.rb +24 -0
- data/lib/fir/util/parser/ipa.rb +188 -0
- data/lib/fir/util/parser/pngcrush.rb +23 -0
- data/lib/fir/util/publish.rb +253 -0
- data/lib/fir/version.rb +5 -0
- data/lib/fir/xcode_wrapper.sh +29 -0
- data/lib/fir_cli.rb +3 -0
- data/test/build_ipa_test.rb +17 -0
- data/test/cases/test_apk.apk +0 -0
- data/test/cases/test_apk_txt +1 -0
- data/test/cases/test_ipa.ipa +0 -0
- data/test/cases/test_ipa_dsym +0 -0
- data/test/info_test.rb +36 -0
- data/test/login_test.rb +12 -0
- data/test/mapping_test.rb +18 -0
- data/test/me_test.rb +17 -0
- data/test/publish_test.rb +44 -0
- data/test/test_helper.rb +98 -0
- metadata +273 -0
data/lib/fir-cli.rb
ADDED
data/lib/fir.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'logger'
|
5
|
+
require 'yaml'
|
6
|
+
require 'rest-client'
|
7
|
+
require 'json'
|
8
|
+
require 'securerandom'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'cfpropertylist'
|
11
|
+
require 'tempfile'
|
12
|
+
require 'rqrcode'
|
13
|
+
|
14
|
+
# TODO: remove rescue when https://github.com/tajchert/ruby_apk/pull/4 merged
|
15
|
+
begin
|
16
|
+
require 'ruby_android'
|
17
|
+
rescue LoadError
|
18
|
+
require 'ruby_apk'
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'fir/patches'
|
22
|
+
require 'fir/util'
|
23
|
+
require 'fir/version'
|
24
|
+
require 'fir/cli'
|
25
|
+
|
26
|
+
module FIR
|
27
|
+
include Util
|
28
|
+
end
|
data/lib/fir/api.yml
ADDED
data/lib/fir/cli.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FIR
|
4
|
+
class CLI < Thor
|
5
|
+
class_option :token, type: :string, aliases: '-T', desc: "User's API Token at fir.im"
|
6
|
+
class_option :logfile, type: :string, aliases: '-L', desc: 'Path to writable logfile'
|
7
|
+
class_option :verbose, type: :boolean, aliases: '-V', desc: 'Show verbose', default: true
|
8
|
+
class_option :quiet, type: :boolean, aliases: '-q', desc: 'Silence commands'
|
9
|
+
class_option :help, type: :boolean, aliases: '-h', desc: 'Show this help message and quit'
|
10
|
+
|
11
|
+
desc '(EXPIRED, PLEASE use fastlane instead) build_ipa BUILD_DIR [options] [settings]', 'Build iOS app (alias: `bi`).'
|
12
|
+
long_desc <<-LONGDESC
|
13
|
+
`build_ipa` command will auto build your project/workspace to an ipa package
|
14
|
+
and it also can auto publish your built ipa to fir.im if use `-p` option.
|
15
|
+
Internally, it use `xcodebuild` to accomplish these things, use `man xcodebuild` to get more information.
|
16
|
+
|
17
|
+
Example:
|
18
|
+
|
19
|
+
$ fir bi <project dir> [-C <configuration>] [-t <target name>] [-o <ipa output dir>] [settings] [-c <changelog>] [-p -Q -T <your api token>]
|
20
|
+
|
21
|
+
$ fir bi <project dir> [-c <changelog> -P <bughd project id> -M -p -Q -T <your api token>]
|
22
|
+
|
23
|
+
$ fir bi <git ssh url> [-B develop -c <changelog> -f <profile> -P <bughd project id> -M -p -Q -T <your api token>]
|
24
|
+
|
25
|
+
$ fir bi <workspace dir> -w -S <scheme name> [-C <configuration>] [-t <target name>] [-o <ipa output dir>] [settings] [-c <changelog>] [-p -Q -T <your api token>]
|
26
|
+
LONGDESC
|
27
|
+
map %w[b bi] => :build_ipa
|
28
|
+
method_option :branch, type: :string, aliases: '-B', desc: 'Set branch if project is a git repo, the default is `master`'
|
29
|
+
method_option :workspace, type: :boolean, aliases: '-w', desc: 'true/false if build workspace'
|
30
|
+
method_option :scheme, type: :string, aliases: '-S', desc: 'Set the scheme NAME if build workspace'
|
31
|
+
method_option :configuration, type: :string, aliases: '-C', desc: 'Use the build configuration NAME for building each target'
|
32
|
+
method_option :destination, type: :string, aliases: '-d', desc: 'Set the destination specifier'
|
33
|
+
method_option :target, type: :string, aliases: '-t', desc: 'Build the target specified by target name'
|
34
|
+
method_option :export_method, type: :string, aliases: '-E', desc: 'for exportOptionsPlist method, ad-hoc as default'
|
35
|
+
method_option :optionPlistPath, type: :string, aliases: '-O', desc: 'User defined exportOptionsPlist path'
|
36
|
+
method_option :profile, type: :string, aliases: '-f', desc: 'Set the export provisioning profile'
|
37
|
+
method_option :output, type: :string, aliases: '-o', desc: 'IPA output path, the default is: BUILD_DIR/fir_build_ipa'
|
38
|
+
method_option :publish, type: :boolean, aliases: '-p', desc: 'true/false if publish to fir.im'
|
39
|
+
method_option :short, type: :string, aliases: '-s', desc: 'Set custom short link if publish to fir.im'
|
40
|
+
method_option :name, type: :string, aliases: '-n', desc: 'Set custom ipa name when built'
|
41
|
+
method_option :changelog, type: :string, aliases: '-c', desc: 'Set changelog if publish to fir.im'
|
42
|
+
method_option :qrcode, type: :boolean, aliases: '-Q', desc: 'Generate qrcode'
|
43
|
+
method_option :mapping, type: :boolean, aliases: '-M', desc: 'true/false if upload app mapping file to BugHD.com'
|
44
|
+
method_option :proj, type: :string, aliases: '-P', desc: 'Project id in BugHD.com if upload app mapping file'
|
45
|
+
method_option :open, type: :boolean, desc: 'true/false if open for everyone'
|
46
|
+
method_option :password, type: :string, desc: 'Set password for app'
|
47
|
+
def build_ipa(*args)
|
48
|
+
prepare :build_ipa
|
49
|
+
|
50
|
+
FIR.build_ipa(*args, options)
|
51
|
+
end
|
52
|
+
|
53
|
+
desc 'build_apk BUILD_DIR', 'Build Android app (alias: `ba`).'
|
54
|
+
long_desc <<-LONGDESC
|
55
|
+
`build_apk` command will auto build your project to an apk package
|
56
|
+
and it also can auto publish your built apk to fir.im if use `-p` option.
|
57
|
+
Internally, it use `gradle` to accomplish these things, use `gradle --help` to get more information.
|
58
|
+
|
59
|
+
Example:
|
60
|
+
|
61
|
+
$ fir ba <project dir> [-o <apk output dir> -c <changelog> -p -Q -T <your api token>]
|
62
|
+
|
63
|
+
$ fir ba <project dir> [-f <flavor> -o <apk output dir> -c <changelog> -p -Q -T <your api token>]
|
64
|
+
|
65
|
+
$ fir ba <git ssh url> [-B develop -o <apk output dir> -c <changelog> -p -Q -T <your api token>]
|
66
|
+
LONGDESC
|
67
|
+
map ['ba'] => :build_apk
|
68
|
+
method_option :branch, type: :string, aliases: '-B', desc: 'Set branch if project is a git repo, the default is `master`'
|
69
|
+
method_option :output, type: :string, aliases: '-o', desc: 'APK output path, the default is: BUILD_DIR/build/outputs/apk'
|
70
|
+
method_option :publish, type: :boolean, aliases: '-p', desc: 'true/false if publish to fir.im'
|
71
|
+
method_option :flavor, type: :string, aliases: '-f', desc: 'Set flavor if have productFlavors'
|
72
|
+
method_option :short, type: :string, aliases: '-s', desc: 'Set custom short link if publish to fir.im'
|
73
|
+
method_option :name, type: :string, aliases: '-n', desc: 'Set custom apk name when builded'
|
74
|
+
method_option :changelog, type: :string, aliases: '-c', desc: 'Set changelog if publish to fir.im, support string/file'
|
75
|
+
method_option :qrcode, type: :boolean, aliases: '-Q', desc: 'Generate qrcode'
|
76
|
+
method_option :open, type: :boolean, desc: 'true/false if open for everyone, the default is: true', default: true
|
77
|
+
method_option :password, type: :string, desc: 'Set password for app'
|
78
|
+
def build_apk(*args)
|
79
|
+
prepare :build_apk
|
80
|
+
|
81
|
+
FIR.build_apk(*args, options)
|
82
|
+
end
|
83
|
+
|
84
|
+
desc 'info APP_FILE_PATH', 'Show iOS/Android app info, support ipa/apk file (aliases: `i`).'
|
85
|
+
map 'i' => :info
|
86
|
+
method_option :all, type: :boolean, aliases: '-a', desc: 'Show all information in application'
|
87
|
+
def info(*args)
|
88
|
+
prepare :info
|
89
|
+
|
90
|
+
FIR.info(*args, options)
|
91
|
+
end
|
92
|
+
|
93
|
+
desc 'publish APP_FILE_PATH', 'Publish iOS/Android app to fir.im, support ipa/apk file (aliases: `p`).'
|
94
|
+
long_desc <<-LONGDESC
|
95
|
+
`publish` command will publish your app file to fir.im, also the command support to publish app's short & changelog.
|
96
|
+
|
97
|
+
|
98
|
+
Example:
|
99
|
+
|
100
|
+
$ fir p <app file path> [-c <changelog> -s <custom short link> -Q -T <your api token>]
|
101
|
+
|
102
|
+
$ fir p <app file path> [-c <changelog> -s <custom short link> --password=123456 --open=false -Q -T <your api token>]
|
103
|
+
|
104
|
+
$ fir p <app file path> [-c <changelog> -s <custom short link> -m <mapping file path> -P <bughd project id> -Q -T <your api token>]
|
105
|
+
LONGDESC
|
106
|
+
map 'p' => :publish
|
107
|
+
method_option :short, type: :string, aliases: '-s', desc: 'Set custom short link'
|
108
|
+
method_option :changelog, type: :string, aliases: '-c', desc: 'Set changelog'
|
109
|
+
method_option :qrcode, type: :boolean, aliases: '-Q', desc: 'Generate qrcode'
|
110
|
+
method_option :need_release_id, type: :boolean, aliases: '-R', desc: 'Add release id with fir url (WARNING: FIR ONLY SAVED 30 releases recently per app'
|
111
|
+
|
112
|
+
method_option :mappingfile, type: :string, aliases: '-m', desc: 'App mapping file'
|
113
|
+
method_option :dingtalk_access_token, type: :string, aliases: '-D', desc: 'Send msg to dingtalk, only need access_token, not whole url'
|
114
|
+
|
115
|
+
method_option :open, type: :boolean, desc: 'true/false if open for everyone'
|
116
|
+
method_option :password, type: :string, desc: 'Set password for app'
|
117
|
+
def publish(*args)
|
118
|
+
prepare :publish
|
119
|
+
|
120
|
+
FIR.publish(*args, options)
|
121
|
+
end
|
122
|
+
|
123
|
+
desc 'login', 'Login fir.im (aliases: `l`).'
|
124
|
+
map 'l' => :login
|
125
|
+
def login(*args)
|
126
|
+
prepare :login
|
127
|
+
|
128
|
+
token = options[:token] || args.first || ask('Please enter your fir.im API Token:', :white, echo: true)
|
129
|
+
FIR.login(token)
|
130
|
+
end
|
131
|
+
|
132
|
+
desc 'me', 'Show current user info if user is logined.'
|
133
|
+
def me
|
134
|
+
prepare :me
|
135
|
+
|
136
|
+
FIR.me
|
137
|
+
end
|
138
|
+
|
139
|
+
desc 'upgrade', 'Upgrade fir-cli and quit (aliases: `u`).'
|
140
|
+
map 'u' => :upgrade
|
141
|
+
def upgrade
|
142
|
+
prepare :upgrade
|
143
|
+
|
144
|
+
say '✈ Upgrade fir-cli (use `gem install fir-cli --no-ri --no-rdoc`)'
|
145
|
+
say `gem install fir-cli --no-ri --no-rdoc`
|
146
|
+
end
|
147
|
+
|
148
|
+
desc 'version', 'Show fir-cli version number and quit (aliases: `v`).'
|
149
|
+
map ['v', '-v', '--version'] => :version
|
150
|
+
def version
|
151
|
+
say "✈ fir-cli #{FIR::VERSION}"
|
152
|
+
end
|
153
|
+
|
154
|
+
desc 'help', 'Describe available commands or one specific command (aliases: `h`).'
|
155
|
+
map Thor::HELP_MAPPINGS => :help
|
156
|
+
def help(command = nil, subcommand = false)
|
157
|
+
super
|
158
|
+
end
|
159
|
+
|
160
|
+
no_commands do
|
161
|
+
def invoke_command(command, *args)
|
162
|
+
logfile = options[:logfile].blank? ? STDOUT : options[:logfile]
|
163
|
+
logfile = '/dev/null' if options[:quiet]
|
164
|
+
|
165
|
+
FIR.logger = Logger.new(logfile)
|
166
|
+
FIR.logger.level = options[:verbose] ? Logger::INFO : Logger::ERROR
|
167
|
+
super
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def prepare(task)
|
174
|
+
if options.help?
|
175
|
+
help(task.to_s)
|
176
|
+
raise SystemExit
|
177
|
+
end
|
178
|
+
$DEBUG = true if ENV['DEBUG']
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/lib/fir/patches.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative './patches/blank'
|
4
|
+
require_relative './patches/concern'
|
5
|
+
require_relative './patches/hash'
|
6
|
+
require_relative './patches/instance_variables'
|
7
|
+
require_relative './patches/native_patch'
|
8
|
+
require_relative './patches/os_patch'
|
9
|
+
require_relative './patches/try'
|
10
|
+
require_relative './patches/default_headers'
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Object
|
4
|
+
# An object is blank if it's false, empty, or a whitespace string.
|
5
|
+
# For example, '', ' ', +nil+, [], and {} are all blank.
|
6
|
+
#
|
7
|
+
# This simplifies
|
8
|
+
#
|
9
|
+
# address.nil? || address.empty?
|
10
|
+
#
|
11
|
+
# to
|
12
|
+
#
|
13
|
+
# address.blank?
|
14
|
+
#
|
15
|
+
# @return [true, false]
|
16
|
+
def blank?
|
17
|
+
respond_to?(:empty?) ? !!empty? : !self
|
18
|
+
end
|
19
|
+
|
20
|
+
# An object is present if it's not blank.
|
21
|
+
#
|
22
|
+
# @return [true, false]
|
23
|
+
def present?
|
24
|
+
!blank?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the receiver if it's present otherwise returns +nil+.
|
28
|
+
# <tt>object.presence</tt> is equivalent to
|
29
|
+
#
|
30
|
+
# object.present? ? object : nil
|
31
|
+
#
|
32
|
+
# For example, something like
|
33
|
+
#
|
34
|
+
# state = params[:state] if params[:state].present?
|
35
|
+
# country = params[:country] if params[:country].present?
|
36
|
+
# region = state || country || 'US'
|
37
|
+
#
|
38
|
+
# becomes
|
39
|
+
#
|
40
|
+
# region = params[:state].presence || params[:country].presence || 'US'
|
41
|
+
#
|
42
|
+
# @return [Object]
|
43
|
+
def presence
|
44
|
+
self if present?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class NilClass
|
49
|
+
# +nil+ is blank:
|
50
|
+
#
|
51
|
+
# nil.blank? # => true
|
52
|
+
#
|
53
|
+
# @return [true]
|
54
|
+
def blank?
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class FalseClass
|
60
|
+
# +false+ is blank:
|
61
|
+
#
|
62
|
+
# false.blank? # => true
|
63
|
+
#
|
64
|
+
# @return [true]
|
65
|
+
def blank?
|
66
|
+
true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class TrueClass
|
71
|
+
# +true+ is not blank:
|
72
|
+
#
|
73
|
+
# true.blank? # => false
|
74
|
+
#
|
75
|
+
# @return [false]
|
76
|
+
def blank?
|
77
|
+
false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Array
|
82
|
+
# An array is blank if it's empty:
|
83
|
+
#
|
84
|
+
# [].blank? # => true
|
85
|
+
# [1,2,3].blank? # => false
|
86
|
+
#
|
87
|
+
# @return [true, false]
|
88
|
+
alias_method :blank?, :empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
class Hash
|
92
|
+
# A hash is blank if it's empty:
|
93
|
+
#
|
94
|
+
# {}.blank? # => true
|
95
|
+
# { key: 'value' }.blank? # => false
|
96
|
+
#
|
97
|
+
# @return [true, false]
|
98
|
+
alias_method :blank?, :empty?
|
99
|
+
end
|
100
|
+
|
101
|
+
class String
|
102
|
+
BLANK_RE = /\A[[:space:]]*\z/
|
103
|
+
|
104
|
+
# A string is blank if it's empty or contains whitespaces only:
|
105
|
+
#
|
106
|
+
# ''.blank? # => true
|
107
|
+
# ' '.blank? # => true
|
108
|
+
# "\t\n\r".blank? # => true
|
109
|
+
# ' blah '.blank? # => false
|
110
|
+
#
|
111
|
+
# Unicode whitespace is supported:
|
112
|
+
#
|
113
|
+
# "\u00a0".blank? # => true
|
114
|
+
#
|
115
|
+
# @return [true, false]
|
116
|
+
def blank?
|
117
|
+
BLANK_RE === self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class Numeric #:nodoc:
|
122
|
+
# No number is blank:
|
123
|
+
#
|
124
|
+
# 1.blank? # => false
|
125
|
+
# 0.blank? # => false
|
126
|
+
#
|
127
|
+
# @return [false]
|
128
|
+
def blank?
|
129
|
+
false
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
# A typical module looks like this:
|
5
|
+
#
|
6
|
+
# module M
|
7
|
+
# def self.included(base)
|
8
|
+
# base.extend ClassMethods
|
9
|
+
# base.class_eval do
|
10
|
+
# scope :disabled, -> { where(disabled: true) }
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# module ClassMethods
|
15
|
+
# ...
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
20
|
+
# written as:
|
21
|
+
#
|
22
|
+
# require 'active_support/concern'
|
23
|
+
#
|
24
|
+
# module M
|
25
|
+
# extend ActiveSupport::Concern
|
26
|
+
#
|
27
|
+
# included do
|
28
|
+
# scope :disabled, -> { where(disabled: true) }
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# class_methods do
|
32
|
+
# ...
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
37
|
+
# and a +Bar+ module which depends on the former, we would typically write the
|
38
|
+
# following:
|
39
|
+
#
|
40
|
+
# module Foo
|
41
|
+
# def self.included(base)
|
42
|
+
# base.class_eval do
|
43
|
+
# def self.method_injected_by_foo
|
44
|
+
# ...
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# module Bar
|
51
|
+
# def self.included(base)
|
52
|
+
# base.method_injected_by_foo
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# class Host
|
57
|
+
# include Foo # We need to include this dependency for Bar
|
58
|
+
# include Bar # Bar is the module that Host really needs
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
62
|
+
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
63
|
+
#
|
64
|
+
# module Bar
|
65
|
+
# include Foo
|
66
|
+
# def self.included(base)
|
67
|
+
# base.method_injected_by_foo
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# class Host
|
72
|
+
# include Bar
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
76
|
+
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
77
|
+
# module dependencies are properly resolved:
|
78
|
+
#
|
79
|
+
# require 'active_support/concern'
|
80
|
+
#
|
81
|
+
# module Foo
|
82
|
+
# extend ActiveSupport::Concern
|
83
|
+
# included do
|
84
|
+
# def self.method_injected_by_foo
|
85
|
+
# ...
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# module Bar
|
91
|
+
# extend ActiveSupport::Concern
|
92
|
+
# include Foo
|
93
|
+
#
|
94
|
+
# included do
|
95
|
+
# self.method_injected_by_foo
|
96
|
+
# end
|
97
|
+
# end
|
98
|
+
#
|
99
|
+
# class Host
|
100
|
+
# include Bar # It works, now Bar takes care of its dependencies
|
101
|
+
# end
|
102
|
+
module Concern
|
103
|
+
class MultipleIncludedBlocks < StandardError #:nodoc:
|
104
|
+
def initialize
|
105
|
+
super "Cannot define multiple 'included' blocks for a Concern"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.extended(base) #:nodoc:
|
110
|
+
base.instance_variable_set(:@_dependencies, [])
|
111
|
+
end
|
112
|
+
|
113
|
+
def append_features(base)
|
114
|
+
if base.instance_variable_defined?(:@_dependencies)
|
115
|
+
base.instance_variable_get(:@_dependencies) << self
|
116
|
+
return false
|
117
|
+
else
|
118
|
+
return false if base < self
|
119
|
+
@_dependencies.each { |dep| base.send(:include, dep) }
|
120
|
+
super
|
121
|
+
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
122
|
+
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def included(base = nil, &block)
|
127
|
+
if base.nil?
|
128
|
+
fail MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
129
|
+
|
130
|
+
@_included_block = block
|
131
|
+
else
|
132
|
+
super
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def class_methods(&class_methods_module_definition)
|
137
|
+
if const_defined?(:ClassMethods, false)
|
138
|
+
mod = const_get(:ClassMethods)
|
139
|
+
else
|
140
|
+
mod = const_set(:ClassMethods, Module.new)
|
141
|
+
end
|
142
|
+
|
143
|
+
mod.module_eval(&class_methods_module_definition)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|