rbxx 0.1.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +72 -0
  3. data/exe/rbxx +6 -0
  4. data/ext-cmake/FindRuby.cmake +42 -0
  5. data/ext-cmake/rbxx-config.cmake +25 -0
  6. data/include/rbxx/arg.hpp +347 -0
  7. data/include/rbxx/callback.hpp +54 -0
  8. data/include/rbxx/class.hpp +630 -0
  9. data/include/rbxx/data_object.hpp +221 -0
  10. data/include/rbxx/detail/ruby_include.hpp +36 -0
  11. data/include/rbxx/exception.hpp +116 -0
  12. data/include/rbxx/extension.hpp +23 -0
  13. data/include/rbxx/function.hpp +463 -0
  14. data/include/rbxx/module.hpp +69 -0
  15. data/include/rbxx/nogvl.hpp +146 -0
  16. data/include/rbxx/object.hpp +68 -0
  17. data/include/rbxx/operators.hpp +31 -0
  18. data/include/rbxx/policies.hpp +36 -0
  19. data/include/rbxx/protect.hpp +72 -0
  20. data/include/rbxx/rbxx.hpp +20 -0
  21. data/include/rbxx/registry.hpp +145 -0
  22. data/include/rbxx/stl/array.hpp +40 -0
  23. data/include/rbxx/stl/chrono.hpp +26 -0
  24. data/include/rbxx/stl/detail.hpp +60 -0
  25. data/include/rbxx/stl/filesystem.hpp +21 -0
  26. data/include/rbxx/stl/map.hpp +49 -0
  27. data/include/rbxx/stl/optional.hpp +23 -0
  28. data/include/rbxx/stl/set.hpp +30 -0
  29. data/include/rbxx/stl/tuple.hpp +74 -0
  30. data/include/rbxx/stl/variant.hpp +35 -0
  31. data/include/rbxx/stl/vector.hpp +34 -0
  32. data/include/rbxx/stl.hpp +11 -0
  33. data/include/rbxx/type_caster.hpp +247 -0
  34. data/include/rbxx/value.hpp +63 -0
  35. data/include/rbxx/version.hpp +14 -0
  36. data/lib/rbxx/cli.rb +127 -0
  37. data/lib/rbxx/mkmf.rb +144 -0
  38. data/lib/rbxx/rake_tasks.rb +44 -0
  39. data/lib/rbxx/version.rb +5 -0
  40. data/lib/rbxx.rb +8 -0
  41. data/single_include/rbxx/rbxx.hpp +3211 -0
  42. data/templates/CMakeLists.txt.erb +14 -0
  43. data/templates/Gemfile.erb +10 -0
  44. data/templates/README.md.erb +11 -0
  45. data/templates/Rakefile.erb +23 -0
  46. data/templates/ci.yml.erb +21 -0
  47. data/templates/extconf.rb.erb +5 -0
  48. data/templates/extconf_cmake.rb.erb +5 -0
  49. data/templates/extension.cpp.erb +23 -0
  50. data/templates/gemspec.erb +16 -0
  51. data/templates/lib.rb.erb +4 -0
  52. data/templates/release.yml.erb +21 -0
  53. data/templates/test.rb.erb +12 -0
  54. data/templates/version.rb.erb +5 -0
  55. metadata +99 -0
@@ -0,0 +1,74 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/stl/detail.hpp>
4
+
5
+ #include <tuple>
6
+ #include <utility>
7
+
8
+ namespace rbxx {
9
+ namespace detail {
10
+ template <typename First, typename Second>
11
+ struct is_non_bindable_class<std::pair<First, Second>> : std::true_type {};
12
+ template <typename... Items> struct is_non_bindable_class<std::tuple<Items...>> : std::true_type {};
13
+
14
+ inline void require_tuple_length(VALUE array, long expected) {
15
+ if (RARRAY_LEN(array) == expected) {
16
+ return;
17
+ }
18
+ std::string message = "rbxx: expected Array tuple of length ";
19
+ message += std::to_string(expected);
20
+ message += ", got ";
21
+ message += std::to_string(RARRAY_LEN(array));
22
+ throw ruby_error(make_exception(rb_eArgError, message.c_str()));
23
+ }
24
+
25
+ template <typename Tuple, std::size_t... Index>
26
+ Tuple load_tuple(VALUE array, std::index_sequence<Index...>) {
27
+ return Tuple{from_ruby<std::tuple_element_t<Index, Tuple>>(
28
+ value{RARRAY_AREF(array, static_cast<long>(Index))})...};
29
+ }
30
+
31
+ template <typename Tuple, std::size_t... Index>
32
+ value dump_tuple(const Tuple& input, std::index_sequence<Index...>) {
33
+ return value{protect([&input] {
34
+ VALUE result = rb_ary_new_capa(static_cast<long>(sizeof...(Index)));
35
+ (rb_ary_push(result, to_ruby(std::get<Index>(input)).raw()), ...);
36
+ return result;
37
+ })};
38
+ }
39
+ } // namespace detail
40
+
41
+ template <typename First, typename Second> struct type_caster<std::pair<First, Second>> {
42
+ using pair_type = std::pair<First, Second>;
43
+ static constexpr std::string_view name = "Array(2)";
44
+ static pair_type load(value input) {
45
+ VALUE array = detail::coerce_array(input);
46
+ detail::require_tuple_length(array, 2);
47
+ return {from_ruby<First>(value{RARRAY_AREF(array, 0)}),
48
+ from_ruby<Second>(value{RARRAY_AREF(array, 1)})};
49
+ }
50
+ static value dump(const pair_type& input) {
51
+ return detail::dump_tuple(input, std::index_sequence<0, 1>{});
52
+ }
53
+ static bool matches(value input) noexcept {
54
+ return input.is_array() && RARRAY_LEN(input.raw()) == 2;
55
+ }
56
+ };
57
+
58
+ template <typename... Items> struct type_caster<std::tuple<Items...>> {
59
+ using tuple_type = std::tuple<Items...>;
60
+ static constexpr std::string_view name = "Array tuple";
61
+ static tuple_type load(value input) {
62
+ VALUE array = detail::coerce_array(input);
63
+ detail::require_tuple_length(array, static_cast<long>(sizeof...(Items)));
64
+ return detail::load_tuple<tuple_type>(array, std::index_sequence_for<Items...>{});
65
+ }
66
+ static value dump(const tuple_type& input) {
67
+ return detail::dump_tuple(input, std::index_sequence_for<Items...>{});
68
+ }
69
+ static bool matches(value input) noexcept {
70
+ return input.is_array() && RARRAY_LEN(input.raw()) == static_cast<long>(sizeof...(Items));
71
+ }
72
+ };
73
+
74
+ } // namespace rbxx
@@ -0,0 +1,35 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/stl/detail.hpp>
4
+
5
+ #include <variant>
6
+
7
+ namespace rbxx {
8
+ namespace detail {
9
+ template <typename... Items>
10
+ struct is_non_bindable_class<std::variant<Items...>> : std::true_type {};
11
+
12
+ template <typename Variant, std::size_t Index = 0> Variant load_variant(value input) {
13
+ if constexpr (Index == std::variant_size_v<Variant>) {
14
+ throw_type_error("one of the std::variant alternatives", input);
15
+ } else {
16
+ using alternative = std::variant_alternative_t<Index, Variant>;
17
+ if (type_caster<alternative>::matches(input)) {
18
+ return Variant{std::in_place_index<Index>, from_ruby<alternative>(input)};
19
+ }
20
+ return load_variant<Variant, Index + 1U>(input);
21
+ }
22
+ }
23
+ } // namespace detail
24
+
25
+ template <typename... Items> struct type_caster<std::variant<Items...>> {
26
+ using variant_type = std::variant<Items...>;
27
+ static constexpr std::string_view name = "variant";
28
+ static variant_type load(value input) { return detail::load_variant<variant_type>(input); }
29
+ static value dump(const variant_type& input) {
30
+ return std::visit([](const auto& selected) { return to_ruby(selected); }, input);
31
+ }
32
+ static bool matches(value input) noexcept { return (type_caster<Items>::matches(input) || ...); }
33
+ };
34
+
35
+ } // namespace rbxx
@@ -0,0 +1,34 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/stl/detail.hpp>
4
+
5
+ #include <vector>
6
+
7
+ namespace rbxx {
8
+ namespace detail {
9
+ template <typename T, typename Allocator>
10
+ struct is_non_bindable_class<std::vector<T, Allocator>> : std::true_type {};
11
+ } // namespace detail
12
+
13
+ template <typename T, typename Allocator> struct type_caster<std::vector<T, Allocator>> {
14
+ static constexpr std::string_view name = "Array";
15
+
16
+ static std::vector<T, Allocator> load(value input) {
17
+ VALUE array = detail::coerce_array(input);
18
+ const auto length = RARRAY_LEN(array);
19
+ std::vector<T, Allocator> result;
20
+ result.reserve(static_cast<std::size_t>(length));
21
+ for (long index = 0; index < length; ++index) {
22
+ result.push_back(from_ruby<T>(value{RARRAY_AREF(array, index)}));
23
+ }
24
+ return result;
25
+ }
26
+
27
+ static value dump(const std::vector<T, Allocator>& input) {
28
+ return detail::dump_array_range(input);
29
+ }
30
+
31
+ static bool matches(value input) noexcept { return input.is_array(); }
32
+ };
33
+
34
+ } // namespace rbxx
@@ -0,0 +1,11 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/stl/array.hpp>
4
+ #include <rbxx/stl/chrono.hpp>
5
+ #include <rbxx/stl/filesystem.hpp>
6
+ #include <rbxx/stl/map.hpp>
7
+ #include <rbxx/stl/optional.hpp>
8
+ #include <rbxx/stl/set.hpp>
9
+ #include <rbxx/stl/tuple.hpp>
10
+ #include <rbxx/stl/variant.hpp>
11
+ #include <rbxx/stl/vector.hpp>
@@ -0,0 +1,247 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/protect.hpp>
4
+
5
+ #include <concepts>
6
+ #include <cstddef>
7
+ #include <limits>
8
+ #include <string>
9
+ #include <string_view>
10
+ #include <type_traits>
11
+ #include <utility>
12
+
13
+ namespace rbxx {
14
+
15
+ /// @brief Customization point for conversion between Ruby and C++ values.
16
+ /// @code template <> struct rbxx::type_caster<MyType> { /* load/dump/matches */ }; @endcode
17
+ template <typename T, typename Enable = void> struct type_caster;
18
+
19
+ /// @brief True when T can be converted to a Ruby value.
20
+ template <typename T>
21
+ concept to_ruby_convertible = requires(T&& input) {
22
+ {
23
+ type_caster<std::remove_cvref_t<T>>::dump(std::forward<T>(input))
24
+ } -> std::convertible_to<value>;
25
+ };
26
+
27
+ /// @brief True when a Ruby value can be converted to T.
28
+ template <typename T>
29
+ concept from_ruby_convertible =
30
+ requires(value input) { type_caster<std::remove_cvref_t<T>>::load(input); };
31
+
32
+ namespace detail {
33
+
34
+ template <typename T> constexpr std::string_view type_name() noexcept {
35
+ #if defined(_MSC_VER)
36
+ constexpr std::string_view signature = __FUNCSIG__;
37
+ constexpr std::string_view prefix = "type_name<";
38
+ const auto start = signature.find(prefix) + prefix.size();
39
+ return signature.substr(start, signature.find(">(void)", start) - start);
40
+ #else
41
+ constexpr std::string_view signature = __PRETTY_FUNCTION__;
42
+ constexpr std::string_view prefix = "T = ";
43
+ const auto start = signature.find(prefix) + prefix.size();
44
+ const auto end = signature.find_first_of("];", start);
45
+ return signature.substr(start, end - start);
46
+ #endif
47
+ }
48
+
49
+ [[noreturn]] inline void throw_type_error(const char* expected, value actual) {
50
+ const char* actual_name = rb_obj_classname(actual.raw());
51
+ std::string message = "rbxx: expected ";
52
+ message += expected;
53
+ message += ", got ";
54
+ message += actual_name;
55
+ throw ruby_error(make_exception(rb_eTypeError, message.c_str()));
56
+ }
57
+
58
+ template <typename Integer> Integer load_integer(value input) {
59
+ if constexpr (std::is_signed_v<Integer>) {
60
+ const auto converted = protect(rb_num2ll, input.raw());
61
+ constexpr auto minimum = static_cast<LONG_LONG>(std::numeric_limits<Integer>::min());
62
+ constexpr auto maximum = static_cast<LONG_LONG>(std::numeric_limits<Integer>::max());
63
+ if (converted < minimum || converted > maximum) {
64
+ throw std::range_error("rbxx: Integer is outside the target C++ signed integer range");
65
+ }
66
+ return static_cast<Integer>(converted);
67
+ } else {
68
+ const auto converted = protect(rb_num2ull, input.raw());
69
+ constexpr auto maximum = static_cast<unsigned LONG_LONG>(std::numeric_limits<Integer>::max());
70
+ if (converted > maximum) {
71
+ throw std::range_error("rbxx: Integer is outside the target C++ unsigned integer range");
72
+ }
73
+ return static_cast<Integer>(converted);
74
+ }
75
+ }
76
+
77
+ template <typename Integer> value dump_integer(Integer input) {
78
+ if constexpr (std::is_signed_v<Integer>) {
79
+ if (RB_FIXABLE(static_cast<LONG_LONG>(input))) {
80
+ return value{LONG2FIX(static_cast<long>(input))};
81
+ }
82
+ return value{protect(rb_ll2inum, static_cast<LONG_LONG>(input))};
83
+ } else {
84
+ if (RB_POSFIXABLE(static_cast<unsigned LONG_LONG>(input))) {
85
+ return value{LONG2FIX(static_cast<long>(input))};
86
+ }
87
+ return value{protect(rb_ull2inum, static_cast<unsigned LONG_LONG>(input))};
88
+ }
89
+ }
90
+
91
+ inline const char* checked_string_pointer(value input) {
92
+ if (!input.is_string()) {
93
+ throw_type_error("String", input);
94
+ }
95
+ VALUE raw = input.raw();
96
+ return protect([raw]() mutable {
97
+ VALUE string = raw;
98
+ return StringValueCStr(string);
99
+ });
100
+ }
101
+
102
+ inline VALUE checked_string_value(value input) {
103
+ if (input.is_string()) {
104
+ return input.raw();
105
+ }
106
+ VALUE converted = protect(rb_check_string_type, input.raw());
107
+ if (NIL_P(converted)) {
108
+ throw_type_error("String or object responding to #to_str", input);
109
+ }
110
+ return converted;
111
+ }
112
+
113
+ inline value dump_utf8(const char* bytes, std::size_t size) {
114
+ if (size > static_cast<std::size_t>(std::numeric_limits<long>::max())) {
115
+ throw std::length_error("rbxx: String is too large for CRuby");
116
+ }
117
+ return value{protect(rb_utf8_str_new, bytes, static_cast<long>(size))};
118
+ }
119
+
120
+ } // namespace detail
121
+
122
+ template <typename T>
123
+ struct type_caster<T, std::enable_if_t<std::is_integral_v<T> && !std::is_same_v<T, bool>>> {
124
+ static constexpr std::string_view name = "Integer";
125
+
126
+ /// @brief Loads a range-checked C++ integer.
127
+ static T load(value input) { return detail::load_integer<T>(input); }
128
+
129
+ /// @brief Dumps a C++ integer as a Ruby Integer.
130
+ static value dump(T input) { return detail::dump_integer(input); }
131
+
132
+ /// @brief Returns whether the Ruby value is an Integer.
133
+ static bool matches(value input) noexcept { return input.is_integer(); }
134
+ };
135
+
136
+ template <> struct type_caster<bool> {
137
+ static constexpr std::string_view name = "Boolean";
138
+
139
+ static bool load(value input) {
140
+ if (!input.is_bool()) {
141
+ detail::throw_type_error("true or false", input);
142
+ }
143
+ return input.raw() == Qtrue;
144
+ }
145
+
146
+ static value dump(bool input) noexcept { return value{input ? Qtrue : Qfalse}; }
147
+ static bool matches(value input) noexcept { return input.is_bool(); }
148
+ };
149
+
150
+ template <> struct type_caster<float> {
151
+ static constexpr std::string_view name = "Float";
152
+ static float load(value input) {
153
+ if (input.is_float()) {
154
+ return static_cast<float>(RFLOAT_VALUE(input.raw()));
155
+ }
156
+ return static_cast<float>(protect(rb_num2dbl, input.raw()));
157
+ }
158
+ static value dump(float input) {
159
+ return value{protect(rb_float_new, static_cast<double>(input))};
160
+ }
161
+ static bool matches(value input) noexcept { return input.is_float() || input.is_integer(); }
162
+ };
163
+
164
+ template <> struct type_caster<double> {
165
+ static constexpr std::string_view name = "Float";
166
+ static double load(value input) {
167
+ return input.is_float() ? RFLOAT_VALUE(input.raw()) : protect(rb_num2dbl, input.raw());
168
+ }
169
+ static value dump(double input) { return value{protect(rb_float_new, input)}; }
170
+ static bool matches(value input) noexcept { return input.is_float() || input.is_integer(); }
171
+ };
172
+
173
+ template <> struct type_caster<const char*> {
174
+ static constexpr std::string_view name = "String";
175
+ static const char* load(value input) { return detail::checked_string_pointer(input); }
176
+ static value dump(const char* input) {
177
+ if (input == nullptr) {
178
+ detail::throw_type_error("non-null C string", value{Qnil});
179
+ }
180
+ return value{protect(rb_utf8_str_new_cstr, input)};
181
+ }
182
+ static bool matches(value input) noexcept { return input.is_string(); }
183
+ };
184
+
185
+ template <> struct type_caster<std::string> {
186
+ static constexpr std::string_view name = "String";
187
+ static std::string load(value input) {
188
+ VALUE converted = detail::checked_string_value(input);
189
+ const char* bytes = protect([converted]() mutable {
190
+ VALUE string = converted;
191
+ return StringValueCStr(string);
192
+ });
193
+ return std::string(bytes, static_cast<std::size_t>(RSTRING_LEN(converted)));
194
+ }
195
+ static value dump(const std::string& input) {
196
+ return detail::dump_utf8(input.data(), input.size());
197
+ }
198
+ static bool matches(value input) noexcept { return input.is_string(); }
199
+ };
200
+
201
+ template <> struct type_caster<std::string_view> {
202
+ static constexpr std::string_view name = "String";
203
+ static std::string_view load(value input) {
204
+ const char* bytes = detail::checked_string_pointer(input);
205
+ return std::string_view(bytes, static_cast<std::size_t>(RSTRING_LEN(input.raw())));
206
+ }
207
+ static value dump(std::string_view input) {
208
+ return detail::dump_utf8(input.data(), input.size());
209
+ }
210
+ static bool matches(value input) noexcept { return input.is_string(); }
211
+ };
212
+
213
+ template <> struct type_caster<value> {
214
+ static constexpr std::string_view name = "Object";
215
+ static value load(value input) noexcept { return input; }
216
+ static value dump(value input) noexcept { return input; }
217
+ static bool matches(value) noexcept { return true; }
218
+ };
219
+
220
+ template <> struct type_caster<object> {
221
+ static constexpr std::string_view name = "Object";
222
+ static object load(value input) { return object{input}; }
223
+ static value dump(const object& input) noexcept { return input.get(); }
224
+ static bool matches(value) noexcept { return true; }
225
+ };
226
+
227
+ /// @brief Converts a C++ value to Ruby with an actionable missing-caster diagnostic.
228
+ /// @code rbxx::value result = rbxx::to_ruby(42); @endcode
229
+ template <typename T> [[nodiscard]] value to_ruby(T&& input) {
230
+ using converted_type = std::remove_cvref_t<T>;
231
+ static_assert(to_ruby_convertible<converted_type>,
232
+ "rbxx: type has no type_caster; bind it with def_class<T>() or specialize "
233
+ "rbxx::type_caster<T>");
234
+ return type_caster<converted_type>::dump(std::forward<T>(input));
235
+ }
236
+
237
+ /// @brief Converts a Ruby value to C++ with an actionable missing-caster diagnostic.
238
+ /// @code int result = rbxx::from_ruby<int>(ruby_value); @endcode
239
+ template <typename T> [[nodiscard]] decltype(auto) from_ruby(value input) {
240
+ using converted_type = std::remove_cvref_t<T>;
241
+ static_assert(from_ruby_convertible<converted_type>,
242
+ "rbxx: type has no type_caster; bind it with def_class<T>() or specialize "
243
+ "rbxx::type_caster<T>");
244
+ return type_caster<converted_type>::load(input);
245
+ }
246
+
247
+ } // namespace rbxx
@@ -0,0 +1,63 @@
1
+ #pragma once
2
+
3
+ #include <rbxx/detail/ruby_include.hpp>
4
+
5
+ #include <type_traits>
6
+
7
+ namespace rbxx {
8
+
9
+ /// @brief A zero-overhead, non-owning view of a Ruby VALUE.
10
+ /// @code rbxx::value nil{Qnil}; @endcode
11
+ class value {
12
+ public:
13
+ /// @brief Constructs a nil value.
14
+ constexpr value() noexcept = default;
15
+
16
+ /// @brief Wraps a raw Ruby VALUE without pinning it.
17
+ /// @code auto wrapped = rbxx::value{Qtrue}; @endcode
18
+ explicit constexpr value(VALUE raw) noexcept : raw_(raw) {}
19
+
20
+ /// @brief Returns the wrapped Ruby VALUE.
21
+ /// @code VALUE raw = wrapped.raw(); @endcode
22
+ [[nodiscard]] constexpr VALUE raw() const noexcept { return raw_; }
23
+
24
+ /// @brief Returns whether this value is nil.
25
+ /// @code if (wrapped.is_nil()) { return; } @endcode
26
+ [[nodiscard]] constexpr bool is_nil() const noexcept { return NIL_P(raw_); }
27
+
28
+ /// @brief Returns whether this value is a Ruby boolean.
29
+ [[nodiscard]] constexpr bool is_bool() const noexcept { return raw_ == Qtrue || raw_ == Qfalse; }
30
+
31
+ /// @brief Returns whether this value is a Ruby Integer.
32
+ [[nodiscard]] bool is_integer() const noexcept { return RB_INTEGER_TYPE_P(raw_); }
33
+
34
+ /// @brief Returns whether this value is a Ruby Float.
35
+ [[nodiscard]] bool is_float() const noexcept { return RB_TYPE_P(raw_, T_FLOAT); }
36
+
37
+ /// @brief Returns whether this value is a Ruby String.
38
+ [[nodiscard]] bool is_string() const noexcept { return RB_TYPE_P(raw_, T_STRING); }
39
+
40
+ /// @brief Returns whether this value is a Ruby Symbol.
41
+ [[nodiscard]] bool is_symbol() const noexcept { return SYMBOL_P(raw_); }
42
+
43
+ /// @brief Returns whether this value is a Ruby Array.
44
+ [[nodiscard]] bool is_array() const noexcept { return RB_TYPE_P(raw_, T_ARRAY); }
45
+
46
+ /// @brief Returns whether this value is a Ruby Hash.
47
+ [[nodiscard]] bool is_hash() const noexcept { return RB_TYPE_P(raw_, T_HASH); }
48
+
49
+ private:
50
+ VALUE raw_ = Qnil;
51
+ };
52
+
53
+ static_assert(std::is_trivially_copyable_v<value>);
54
+ static_assert(sizeof(value) == sizeof(VALUE));
55
+
56
+ /// @brief Keeps a temporary Ruby value visible to the conservative GC.
57
+ /// @code rbxx::gc_guard(value); @endcode
58
+ inline void gc_guard(value guarded) noexcept {
59
+ VALUE raw = guarded.raw();
60
+ RB_GC_GUARD(raw);
61
+ }
62
+
63
+ } // namespace rbxx
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ #define RBXX_VERSION_MAJOR 0
4
+ #define RBXX_VERSION_MINOR 1
5
+ #define RBXX_VERSION_PATCH 0
6
+ #define RBXX_VERSION "0.1.0"
7
+
8
+ namespace rbxx {
9
+
10
+ /// @brief The rbxx semantic version string.
11
+ /// @code auto version = rbxx::version; @endcode
12
+ inline constexpr const char* version = RBXX_VERSION;
13
+
14
+ } // namespace rbxx
data/lib/rbxx/cli.rb ADDED
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "fileutils"
5
+ require "optparse"
6
+
7
+ module Rbxx
8
+ # Command-line project generator for source and CMake-based native gems.
9
+ class CLI
10
+ TEMPLATE_ROOT = File.expand_path("../../templates", __dir__)
11
+
12
+ def self.start(arguments, out: $stdout, err: $stderr)
13
+ new(arguments.dup, out:, err:).run
14
+ rescue OptionParser::ParseError, ArgumentError => e
15
+ err.puts(e.message)
16
+ 1
17
+ end
18
+
19
+ def initialize(arguments, out:, err:)
20
+ @arguments = arguments
21
+ @out = out
22
+ @err = err
23
+ end
24
+
25
+ def run
26
+ command = @arguments.shift
27
+ return create_project if command == "new"
28
+
29
+ raise ArgumentError, "usage: rbxx new NAME [--cmake]"
30
+ end
31
+
32
+ private
33
+
34
+ def create_project
35
+ options = parse_options
36
+ name = @arguments.shift
37
+ validate_name(name)
38
+ raise ArgumentError, "unexpected arguments: #{@arguments.join(' ')}" unless @arguments.empty?
39
+
40
+ project = Project.new(name:, cmake: options[:cmake], rbxx_path: options[:rbxx_path])
41
+ project.generate
42
+ @out.puts("Created #{project.destination}")
43
+ 0
44
+ end
45
+
46
+ def parse_options
47
+ options = { cmake: false, rbxx_path: nil }
48
+ OptionParser.new do |parser|
49
+ parser.on("--cmake") { options[:cmake] = true }
50
+ parser.on("--rbxx-path PATH") { |path| options[:rbxx_path] = File.expand_path(path) }
51
+ end.parse!(@arguments)
52
+ options
53
+ end
54
+
55
+ def validate_name(name)
56
+ return if name&.match?(/\A[a-z][a-z0-9_]*\z/)
57
+
58
+ raise ArgumentError, "NAME must use lowercase letters, digits, and underscores"
59
+ end
60
+
61
+ # Rendering context for one generated gem project.
62
+ class Project
63
+ attr_reader :destination
64
+
65
+ def initialize(name:, cmake:, rbxx_path:)
66
+ @name = name
67
+ @cmake = cmake
68
+ @rbxx_path = rbxx_path
69
+ @destination = File.expand_path(name)
70
+ end
71
+
72
+ def generate
73
+ raise ArgumentError, "destination already exists: #{destination}" if File.exist?(destination)
74
+
75
+ templates.each do |source, relative_destination|
76
+ render(source, File.join(destination, relative_destination))
77
+ end
78
+ end
79
+
80
+ def class_name
81
+ @name.split("_").map(&:capitalize).join
82
+ end
83
+
84
+ def rbxx_gem_line
85
+ return %(gem "rbxx", path: #{@rbxx_path.inspect}) if @rbxx_path
86
+
87
+ %(gem "rbxx", "~> 0.1")
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :name
93
+
94
+ def templates
95
+ common_templates.merge(build_templates)
96
+ end
97
+
98
+ def common_templates
99
+ {
100
+ "Gemfile.erb" => "Gemfile",
101
+ "gemspec.erb" => "#{name}.gemspec",
102
+ "Rakefile.erb" => "Rakefile",
103
+ "README.md.erb" => "README.md",
104
+ "extension.cpp.erb" => "ext/#{name}/#{name}.cpp",
105
+ "lib.rb.erb" => "lib/#{name}.rb",
106
+ "version.rb.erb" => "lib/#{name}/version.rb",
107
+ "test.rb.erb" => "test/test_#{name}.rb",
108
+ "ci.yml.erb" => ".github/workflows/ci.yml",
109
+ "release.yml.erb" => ".github/workflows/release.yml"
110
+ }
111
+ end
112
+
113
+ def build_templates
114
+ extconf = @cmake ? "extconf_cmake.rb.erb" : "extconf.rb.erb"
115
+ result = { extconf => "ext/#{name}/extconf.rb" }
116
+ result["CMakeLists.txt.erb"] = "ext/#{name}/CMakeLists.txt" if @cmake
117
+ result
118
+ end
119
+
120
+ def render(source, target)
121
+ FileUtils.mkdir_p(File.dirname(target))
122
+ template = File.read(File.join(TEMPLATE_ROOT, source))
123
+ File.write(target, ERB.new(template, trim_mode: "-").result(binding))
124
+ end
125
+ end
126
+ end
127
+ end